# Alert

> Inline status feedback for info, success, warning, and error states.

A shadcn/ui block from the [@wensity](https://ui.wensity.com/components) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/wensity/alert).

- Page, with a live preview: https://shadcn.exchange/r/wensity/alert
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/wensity/alert-light-986bce7493e34adc.png
- Registry item: https://raw.githubusercontent.com/wensity/registry/main/alert.json
- Categories: UI Primitives, wensity, free
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://raw.githubusercontent.com/wensity/registry/main/alert.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@tabler/icons-react`, `clsx`, `tailwind-merge`

## Notes from the registry

Free Wensity component. Installs to @components/wensity/alert.tsx. For Pro components and updates, use pnpm dlx wensity@latest add alert.

## Files

### `registry/wensity/lib/utils.ts`

```ts
import { type ClassValue, clsx } from "clsx";
import { twMerge } from "tailwind-merge";

export function cn(...inputs: ClassValue[]) {
  return twMerge(clsx(inputs));
}
```

### `registry/wensity/alert.tsx`

```tsx
"use client";

import * as React from "react";
import {
  IconAlertTriangle,
  IconCheck,
  IconCircleX,
  IconInfoCircle,
  IconX,
} from "@tabler/icons-react";
import { cn } from "@/lib/utils";

export type AlertVariant = "neutral" | "info" | "success" | "warning" | "error";
export type AlertDensity = "comfortable" | "compact";

export interface AlertProps extends Omit<React.HTMLAttributes<HTMLDivElement>, "title"> {
  variant?: AlertVariant;
  density?: AlertDensity;
  title?: React.ReactNode;
  description?: React.ReactNode;
  icon?: React.ReactNode;
  action?: React.ReactNode;
  onDismiss?: () => void;
  dismissLabel?: string;
}

export type AlertTitleProps = React.HTMLAttributes<HTMLDivElement>;

export type AlertDescriptionProps = React.HTMLAttributes<HTMLDivElement>;

/** Status-token recipes — source of truth for Phase 7 status system. */
const variantStyles: Record<
  AlertVariant,
  {
    root: string;
    icon: string;
    iconNode: React.ReactNode;
  }
> = {
  neutral: {
    root: cn(
      "border-[var(--primitive-border-subtle)] bg-[var(--primitive-surface-elevated)] text-[var(--foreground)]",
      "[box-shadow:var(--primitive-shadow-raised)]",
    ),
    icon: "text-[var(--primitive-text-secondary)]",
    iconNode: <IconInfoCircle stroke={1.9} />,
  },
  info: {
    root: cn(
      "border-[var(--primitive-info-border)] bg-[var(--primitive-info-surface)] text-[var(--foreground)]",
      "[box-shadow:var(--primitive-shadow-raised)]",
    ),
    icon: "text-[var(--primitive-info)]",
    iconNode: <IconInfoCircle stroke={1.9} />,
  },
  success: {
    root: cn(
      "border-[var(--primitive-success-border)] bg-[var(--primitive-success-surface)] text-[var(--foreground)]",
      "[box-shadow:var(--primitive-shadow-raised)]",
    ),
    icon: "text-[var(--primitive-success)]",
    iconNode: <IconCheck stroke={2.1} />,
  },
  warning: {
    root: cn(
      "border-[var(--primitive-warning-border)] bg-[var(--primitive-warning-surface)] text-[var(--foreground)]",
      "[box-shadow:var(--primitive-shadow-raised)]",
    ),
    icon: "text-[var(--primitive-warning)]",
    iconNode: <IconAlertTriangle stroke={1.9} />,
  },
  error: {
    root: cn(
      "border-[var(--primitive-destructive-border)] bg-[var(--primitive-destructive-surface)] text-[var(--foreground)]",
      "[box-shadow:var(--primitive-shadow-raised)]",
    ),
    icon: "text-[var(--primitive-destructive)]",
    iconNode: <IconCircleX stroke={1.9} />,
  },
};

const densityStyles: Record<AlertDensity, string> = {
  comfortable: "gap-3 px-3.5 py-3",
  compact: "gap-2.5 px-3 py-2.5",
};

function alertRole(variant: AlertVariant): "alert" | "status" {
  return variant === "error" || variant === "warning" ? "alert" : "status";
}

export const Alert = React.forwardRef<HTMLDivElement, AlertProps>(
  (
    {
      className,
      variant = "info",
      density = "comfortable",
      title,
      description,
      icon,
      action,
      onDismiss,
      dismissLabel = "Dismiss alert",
      children,
      ...props
    },
    ref,
  ) => {
    const styles = variantStyles[variant];

    return (
      <div data-wensity-primitive=""
        ref={ref}
        role={alertRole(variant)}
        data-slot="alert"
        data-variant={variant}
        className={cn(
          "wensity-alert relative isolate flex overflow-hidden rounded-[var(--primitive-radius-surface)] border",
          "motion-safe:animate-[wensity-alert-enter_180ms_var(--primitive-ease,cubic-bezier(0.22,1,0.36,1))_both]",
          densityStyles[density],
          styles.root,
          className,
        )}
        {...props}
      >
        <span
          aria-hidden="true"
          className={cn(
            "mt-0.5 flex size-5 shrink-0 items-center justify-center opacity-95 [&_svg]:size-[18px]",
            styles.icon,
          )}
        >
          {icon ?? styles.iconNode}
        </span>
        <div className="min-w-0 flex-1">
          <div className="space-y-1">
            {title ? <AlertTitle>{title}</AlertTitle> : null}
            {description ? <AlertDescription>{description}</AlertDescription> : null}
            {children}
          </div>
        </div>
        {action ? <div className="ml-2 flex shrink-0 items-center self-center">{action}</div> : null}
        {onDismiss ? (
          <button
            type="button"
            aria-label={dismissLabel}
            onClick={onDismiss}
            className={cn(
              "-mr-1 flex size-7 shrink-0 items-center justify-center rounded-[var(--primitive-radius-control-sm)]",
              "text-[var(--primitive-text-secondary)]",
              "transition-[background,color,border-color] duration-150 ease-[var(--primitive-ease,cubic-bezier(0.22,1,0.36,1))]",
              "hover:bg-[var(--primitive-surface-hover)] hover:text-[var(--foreground)]",
              "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[var(--primitive-ring)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--background)]",
            )}
          >
            <IconX aria-hidden="true" className="size-4" stroke={1.9} />
          </button>
        ) : null}
      </div>
    );
  },
);
Alert.displayName = "Alert";

export const AlertTitle = React.forwardRef<HTMLDivElement, AlertTitleProps>(
  ({ className, ...props }, ref) => (
    <div
      ref={ref}
      data-slot="alert-title"
      className={cn(
        "font-[family-name:var(--primitive-font-display)] text-[13px] font-semibold leading-5 tracking-[-0.01em] text-[var(--foreground)]",
        className,
      )}
      {...props}
    />
  ),
);
AlertTitle.displayName = "AlertTitle";

export const AlertDescription = React.forwardRef<HTMLDivElement, AlertDescriptionProps>(
  ({ className, ...props }, ref) => (
    <div
      ref={ref}
      data-slot="alert-description"
      className={cn(
        "text-[12.5px] leading-5 text-[var(--primitive-text-secondary)]",
        className,
      )}
      {...props}
    />
  ),
);
AlertDescription.displayName = "AlertDescription";
```
