# Alert

Info, success, warning and error callouts that slide in and collapse smoothly when dismissed.

**Category:** Feedback  
**Exports:** `Alert`  
**npm dependencies:** motion (+ clsx, tailwind-merge for `cn`)

## Install

```bash
npx shadcn@latest add https://charcoalui.com/r/alert.json
```
The file lands at `components/ui/alert.tsx`; import from `@/components/ui/alert`.

## API — Alert
| Prop | Type | Default | Notes |
| --- | --- | --- | --- |
| `variant` | `"info" \| "success" \| "warning" \| "error"` | `"info"` |  |
| `title` | `ReactNode` | — |  |
| `action` | `ReactNode` | — |  |
| `dismissible` | `boolean` | — |  |
| `onDismiss` | `() => void` | — |  |

## Example — Alert

```tsx
import { Alert } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";

export default function AlertDemo({
  variant = "success",
  title = "Deployed to production",
  dismissible = true,
}: {
  variant?: "info" | "success" | "warning" | "error";
  title?: string;
  dismissible?: boolean;
}) {
  return (
    <div className="grid w-full max-w-md gap-3">
      <Alert variant={variant} title={title} dismissible={dismissible}>
        charcoal-ui is live. It took 42 seconds.
      </Alert>
      <Alert variant="warning" title="Your trial ends in 3 days" action={<Button size="sm" variant="ember">Upgrade</Button>}>
        Keep your 12 projects by choosing a plan.
      </Alert>
      <Alert variant="error" title="We couldn't save your changes" dismissible>
        Check your connection and try again.
      </Alert>
    </div>
  );
}
```

## Source — components/ui/alert.tsx

```tsx
"use client";

import * as React from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";
import { cn } from "@/lib/utils";

type Variant = "info" | "success" | "warning" | "error";

export type AlertProps = {
  variant?: Variant;
  title: React.ReactNode;
  children?: React.ReactNode;
  action?: React.ReactNode;
  /** Show a close button; the alert collapses away when pressed. */
  dismissible?: boolean;
  onDismiss?: () => void;
  className?: string;
};

const styles: Record<Variant, { wrap: string; icon: React.ReactNode }> = {
  info: {
    wrap: "border-border bg-card",
    icon: <path d="M12 16v-4.5M12 8h.01M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />,
  },
  success: {
    wrap: "border-success/30 bg-success/[0.06] [&_[data-icon]]:text-success",
    icon: <path d="m8.5 12.5 2.5 2.5 4.5-5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />,
  },
  warning: {
    wrap: "border-ember/40 bg-ember/[0.06] [&_[data-icon]]:text-ember",
    icon: <path d="M12 9v4M12 16.5h.01M10.3 3.9 2.5 17.5A2 2 0 0 0 4.2 20.5h15.6a2 2 0 0 0 1.7-3L13.7 3.9a2 2 0 0 0-3.4 0Z" />,
  },
  error: {
    wrap: "border-destructive/40 bg-destructive/[0.06] [&_[data-icon]]:text-destructive",
    icon: <path d="M12 7.5v5.5M12 16.5h.01M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z" />,
  },
};

export function Alert({ variant = "info", title, children, action, dismissible, onDismiss, className }: AlertProps) {
  const reduce = useReducedMotion();
  const [open, setOpen] = React.useState(true);
  const s = styles[variant];

  return (
    <AnimatePresence>
      {open && (
        <motion.div
          role={variant === "error" ? "alert" : "status"}
          initial={{ opacity: 0, y: reduce ? 0 : -8, height: 0 }}
          animate={{ opacity: 1, y: 0, height: "auto" }}
          exit={{ opacity: 0, height: 0, transition: { type: "spring", stiffness: 500, damping: 45 } }}
          transition={{ type: "spring", stiffness: 420, damping: 34 }}
          className="overflow-hidden"
        >
          <div className={cn("relative flex gap-3 rounded-xl border p-4", s.wrap, className)}>
            <svg data-icon viewBox="0 0 24 24" className="mt-0.5 size-[18px] shrink-0 text-foreground" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
              {s.icon}
            </svg>
            <div className="min-w-0 flex-1 pr-6">
              <p className="text-sm font-semibold">{title}</p>
              {children && <div className="mt-1 text-sm leading-6 text-muted-foreground">{children}</div>}
              {action && <div className="mt-3 flex gap-2">{action}</div>}
            </div>
            {dismissible && (
              <button
                type="button"
                aria-label="Dismiss"
                onClick={() => {
                  setOpen(false);
                  onDismiss?.();
                }}
                className="absolute right-2.5 top-2.5 grid size-7 cursor-pointer place-items-center rounded-md text-muted-foreground outline-none transition-colors hover:bg-accent hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring"
              >
                <svg viewBox="0 0 24 24" className="size-3.5" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" aria-hidden>
                  <path d="M6 6l12 12M18 6 6 18" />
                </svg>
              </button>
            )}
          </div>
        </motion.div>
      )}
    </AnimatePresence>
  );
}
```