Component · Navigation & Feedback
Alert
Overview
A static callout that draws attention to a piece of information in the flow of a page — a success confirmation, a warning about consequences, an error that needs fixing, a neutral note. Alert is persistent and non-dismissible: it stays until the surrounding content changes. A message that appears transiently, stacks, or auto-dismisses is Toast, not an Alert variant.
Anatomy
Container → optional leading intent icon → content (optional AlertTitle, optional
AlertDescription). A visually hidden intent word (“Note:” / “Success:” / “Warning:” / “Error:”)
precedes the content on every non-default intent, carrying the intent to assistive tech even
though the icon is decorative — see Accessibility. No dismiss button, no action
row: content that needs a “Retry” or “Undo” places that Button inside AlertDescription, and
anything that needs a close affordance is a different component.
Live Example
Intents
Title only
No icon
Dynamically shown — caller adds the live-region role
Code Example
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
const INTENTS = ["default", "info", "success", "warning", "destructive"] as const;
/**
* Live Alert example — Alert is a server component (no client JS). Shows the
* five intents with their per-intent icon and visually hidden intent word, a
* title-only alert, an icon-free alert, and the caller-owned `role` for a
* dynamically shown message (Alert sets no role itself).
*/
export function AlertShowcase() {
return (
<div className="flex flex-col gap-8">
<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Intents
</h3>
<div className="flex flex-col gap-3">
{INTENTS.map((intent) => (
<Alert key={intent} intent={intent}>
<AlertTitle>
{intent[0].toUpperCase() + intent.slice(1)} alert
</AlertTitle>
<AlertDescription>
A short sentence explaining what happened and what, if anything,
to do next.
</AlertDescription>
</Alert>
))}
</div>
</section>
<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Title only
</h3>
<Alert intent="success">
<AlertTitle>Your changes have been saved.</AlertTitle>
</Alert>
</section>
<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
No icon
</h3>
<Alert intent="info" icon={null}>
<AlertTitle>Scheduled maintenance</AlertTitle>
<AlertDescription>
The dashboard will be read-only on Sunday from 02:00–03:00 UTC.
</AlertDescription>
</Alert>
</section>
<section className="flex flex-col gap-3">
<h3 className="text-sm font-medium leading-snug text-muted-foreground">
Dynamically shown — caller adds the live-region role
</h3>
<Alert intent="destructive" role="alert">
<AlertTitle>Payment failed</AlertTitle>
<AlertDescription>
We couldn’t charge your card. Update your payment method and
try again.
</AlertDescription>
</Alert>
</section>
</div>
);
}Variants
One axis: intent. Five values.
| Intent | Surface | Border | Title + icon | Body | Icon |
|---|---|---|---|---|---|
default | --card | --border | --card-foreground | --card-foreground | none |
info | --info-subtle | --info-subtle-border | --info-subtle-foreground | --foreground | Info |
success | --success-subtle | --success-subtle-border | --success-subtle-foreground | --foreground | CircleCheck |
warning | --warning-subtle | --warning-subtle-border | --warning-subtle-foreground | --foreground | TriangleAlert |
destructive | --destructive-subtle | --destructive-subtle-border | --destructive-subtle-foreground | --foreground | OctagonAlert |
defaultis the neutral callout — no colour, no icon, no new token. Use it when the message has no success/warning/error character.- The four feedback intents differ by icon shape and label, not colour alone — the icon is a distinct Lucide glyph per intent and the visually hidden intent word names it for screen readers.
- Body copy stays
--foreground(≥ 18:1 on every subtle surface). Only the title takes the intent colour — a coloured multi-line paragraph reads poorly and is harder to scan. - No size, elevation, or layout variant. An Alert is one width (its container’s), one padding.
Icon control
icon prop | Result |
|---|---|
| omitted | The per-intent default glyph (default gets none). |
null | No icon; the grid collapses to one column. The hidden intent word stays. |
| a node | Rendered as given. Mark it aria-hidden yourself if it is decorative. |
States
| State | Look |
|---|---|
| Default | Intent colours as above. Alert has no interactive states of its own. |
Alert is not interactive — nothing to hover, focus, activate, or disable. Any control inside an Alert (a link, a Button) carries its own states from its own component.
Usage Guidance
Tokens
Alert reads the feedback colour family — twelve semantic tokens (--{intent}-subtle, --{intent}-subtle-foreground, --{intent}-subtle-border for info / success / warning / destructive), defined for light and dark themes — plus existing tokens for the neutral default intent and all geometry. It defines no alert-* tokens of its own; Toast and form-field error surfaces share the same family. See Color — Feedback.
| Token | Where used |
|---|---|
--info-subtle / --info-subtle-foreground / --info-subtle-border | info surface / title + icon / border |
--success-subtle / --success-subtle-foreground / --success-subtle-border | success surface / title + icon / border |
--warning-subtle / --warning-subtle-foreground / --warning-subtle-border | warning surface / title + icon / border |
--destructive-subtle / --destructive-subtle-foreground / --destructive-subtle-border | destructive surface / title + icon / border |
--card / --card-foreground | default surface + text |
--border | default border |
--foreground | Body copy on every feedback intent |
--radius-lg | Corner radius — Radius’s “cards, popovers” |
--scale-px | Border width — Color’s default border recipe |
--scale-4 | Container padding (16px) |
--scale-5 | Icon size (20px) — Icons’s “default UI” size |
--scale-3 | Icon-to-content gap (12px) |
--scale-1 | Gap between title and description |
Do / Don’t
Do
- Match the intent to the message:
successfor a completed action,warningfor a consequence to weigh,destructivefor an error or failure,infofor a neutral heads-up,defaultfor a note with no feedback character. - Give the Alert a short, specific title — “Payment failed,” not “Error.”
- Keep it to one idea. A second unrelated message is a second Alert.
Don’t
- Use Alert for a transient confirmation that should fade on its own — that’s Toast.
- Add a close button — Alert is persistent by contract.
- Rely on the colour alone to carry the intent. The icon + title do that; keep them.
Accessibility
- No implicit ARIA role. Visual intent (how it looks) and announcement urgency (whether a
screen reader interrupts) are separate decisions. A persistent Alert that is on the page at
load needs no live region — it is read in normal document order, and the visually hidden
intent word (“Error:”, “Warning:”…) gives its meaning. Alert never sets
rolefor you. - Dynamically shown Alert: the caller adds the role.
role="status"(polite — waits for a pause) for confirmations and non-urgent info;role="alert"(assertive — interrupts) for an error the user must address now. Pass it as a prop; it lands on the container untouched. - The icon is
aria-hidden— it is visual reinforcement. The intent survives without it via the hidden word and the title text. - Colour is never the only signal: distinct icon shape per intent, plus the intent word.
- Contrast (WCAG 2.1 AA): body
--foregroundand title/icon--{intent}-subtle-foregroundboth clear AA on every--{intent}-subtlesurface, in light and dark — the same feedback family Toast reuses unchanged.