Alert
An inline message with a tone, an optional icon, a title, a body and one action, for a note that stays put inside a page, card or dialog.
Installation
$
npx shadcn@latest add https://ui.mattdowney.com/r/alert.jsonUsage
import { Alert } from "@/components/ui/alert"Examples
Default
The icon sits on the first line of text and takes the tone's colour. Pass an element (<IconInfoCircle />), not a bare component.
Your trial ends Friday
Pick a plan before then to keep your projects and your team.
Tones
Neutral for a tip, info for news, success for a result, warning for something to check, destructive for a failure. Destructive speaks up to screen readers; the rest wait their turn.
Tip
Drag a card to a new column to change its status.
Import queued
We will email you when your 2,400 contacts are in.
Invoice sent
Ava Chen has 30 days to pay.
Two tasks overlap
Design review and Standup are both at 10:00 on Tuesday.
Payment failed
Your card was declined. Update it to keep your plan.
With an action
One action, usually a small Button. It sits beside the title, or under the text when there is no title. Keep it to a single verb.
Flagged for review
This post uses a claim we could not match to a source.
We could not save your changes.
Title or body alone
Either part can stand alone. Without an icon the text runs from the edge.
All changes saved
Only people with the link can see this page.
API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
| tone | "neutral" | "info" | "success" | "warning" | "destructive" | "neutral" | Fill, text and icon colour together. Each pair clears its contrast in light and dark. destructive sets role="alert"; the others set role="status". |
| icon | React.ReactNode | - | An icon element, 16px, on the first line. Hidden from screen readers; the title says what it means. |
| title | React.ReactNode | - | The heading line: what happened, in a few words. |
| children | React.ReactNode | - | The body. Say what to do next, not what went wrong twice. |
| action | React.ReactNode | - | One action, usually <Button size="xs" variant="soft">. Beside the title, or under the body when there is no title. |
| role | string | - | Overrides the tone's default role. |
tone"neutral" | "info" | "success" | "warning" | "destructive" = "neutral"Fill, text and icon colour together. Each pair clears its contrast in light and dark. destructive sets role="alert"; the others set role="status". |
| iconReact.ReactNodeAn icon element, 16px, on the first line. Hidden from screen readers; the title says what it means. |
| titleReact.ReactNodeThe heading line: what happened, in a few words. |
| childrenReact.ReactNodeThe body. Say what to do next, not what went wrong twice. |
actionReact.ReactNodeOne action, usually <Button size="xs" variant="soft">. Beside the title, or under the body when there is no title. |
| rolestringOverrides the tone's default role. |
import * as React from "react"
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "@/registry/lib/utils"
// The tinted tones use the same hue pairs as Badge, one step lighter for the
// fill (-50 / -950) because an alert is a surface, not a chip. Title, body and
// icon each clear their own contrast against it in both themes (title 13:1 or
// better, body 8:1, icon 4.5:1 or better).
const alertVariants = cva(
// The edge is a hairline ring, not a border: an alert sits INSIDE a surface
// (a card, a dialog), so it takes the flat ring and no drop shadow.
"flex items-start gap-3 rounded-md p-3 text-sm shadow-ring-flat",
{
variants: {
tone: {
neutral: "bg-muted text-foreground [--alert-body:var(--muted-foreground)] [--alert-icon:var(--muted-foreground)]",
info: "bg-info-50 dark:bg-info-950 text-info-900 dark:text-info-100 [--alert-body:var(--color-info-800)] dark:[--alert-body:var(--color-info-200)] [--alert-icon:var(--color-info-700)] dark:[--alert-icon:var(--color-info-400)]",
success:
"bg-success-50 dark:bg-success-950 text-success-900 dark:text-success-100 [--alert-body:var(--color-success-800)] dark:[--alert-body:var(--color-success-200)] [--alert-icon:var(--color-success-700)] dark:[--alert-icon:var(--color-success-400)]",
// Warning reads -700 for its icon too: the hue is so light that -600
// on -50 is only 2.7:1, and -700 lands at 4.5:1.
warning:
"bg-warning-50 dark:bg-warning-950 text-warning-900 dark:text-warning-100 [--alert-body:var(--color-warning-800)] dark:[--alert-body:var(--color-warning-200)] [--alert-icon:var(--color-warning-700)] dark:[--alert-icon:var(--color-warning-400)]",
destructive:
"bg-destructive-50 dark:bg-destructive-950 text-destructive-900 dark:text-destructive-100 [--alert-body:var(--color-destructive-800)] dark:[--alert-body:var(--color-destructive-200)] [--alert-icon:var(--color-destructive-700)] dark:[--alert-icon:var(--color-destructive-400)]",
},
},
defaultVariants: { tone: "neutral" },
}
)
type AlertProps = Omit<React.ComponentProps<"div">, "title"> &
VariantProps<typeof alertVariants> & {
/** An icon element (`<IconInfoCircle />`), not a bare component. It sits on
* the first line and takes the tone's colour. Leave it off and the text
* runs from the edge. */
icon?: React.ReactNode
/** The heading line: what happened, in a few words. */
title?: React.ReactNode
/** One action, usually a small Button (`size="xs"` with `variant="soft"`,
* or `link`). Sits beside the title, or under the text when there is no
* title. */
action?: React.ReactNode
}
// @use-when an inline message inside a page, card or dialog that stays put until
// the situation changes: a warning above a form, a "flagged for review" note
// with a title and a paragraph, a result box with one action. Not Sonner or
// Toast, which appear briefly after an action; not AlertDialog, which stops the
// user and asks.
function Alert({ tone, icon, title, action, className, children, role, ...props }: AlertProps) {
const hasTitle = title !== undefined && title !== null && title !== false
const hasBody = children !== undefined && children !== null && children !== false
return (
<div
data-slot="alert"
data-tone={tone ?? "neutral"}
// A failure interrupts a screen reader; the rest wait their turn.
role={role ?? (tone === "destructive" ? "alert" : "status")}
className={cn(alertVariants({ tone }), className)}
{...props}
>
{icon ? (
// One text line tall, so the icon centres on the first line however long
// the message runs. A title row that carries an action is a Button
// tall (24px), so the icon takes that height to stay on the title.
<span
data-slot="alert-icon"
aria-hidden
className={cn(
"flex shrink-0 items-center text-(--alert-icon) [&_svg]:size-4",
hasTitle && action ? "h-6" : "h-[1lh]"
)}
>
{icon}
</span>
) : null}
<div className="flex min-w-0 flex-1 flex-col" style={{ gap: "var(--spacing-rhythm-pair)" }}>
{hasTitle ? (
<div className="flex flex-wrap items-center justify-between gap-x-3 gap-y-1">
<div data-slot="alert-title" className="font-semibold text-pretty">
{title}
</div>
{action ? <div data-slot="alert-action" className="shrink-0">{action}</div> : null}
</div>
) : null}
{hasBody ? (
<div data-slot="alert-body" className="text-pretty text-(--alert-body)">
{children}
</div>
) : null}
{action && !hasTitle ? (
<div data-slot="alert-action" className="pt-1">
{action}
</div>
) : null}
</div>
</div>
)
}
export { Alert, alertVariants }
export type { AlertProps }