a callout in the page's flow: success, warning or error, with an action
npx shadcn add @ag/alertAlready installed? Preview an update
npx shadcn add @ag/alert --dry-runYour installed source stays unchanged until you update it. How updates work
Latest changes · 2026-10-11
- Changed (breaking): no live region by default. A callout of the page is read with the page;
livemakes one that appears because something happened say so (role=alertwhen destructive,role=statusotherwise). Migration: addlivewhere an alert is shown after an action.
the demo above, verbatim
"use client"
import { Sample } from "@/app/samples"
import { Alert } from "@/registry/base-nova/ui/alert"
import { Button } from "@/registry/base-nova/ui/button"
import { MaterialProvider } from "@/registry/base-nova/ui/material"
// #region alerts
function Alerts() {
return (
<div className="grid max-w-3xl gap-3 sm:grid-cols-2">
<Alert title="the vault is unmounted">
none of its files are shown until it is opened again.
</Alert>
<Alert kind="success" title="35 frames read">
smile found in 4 of them.
</Alert>
<Alert kind="warning" title="the archive disk is offline">
its 8 files keep their tags; they play again once it is back.
</Alert>
<Alert
kind="destructive"
title="the upload stopped"
action={
<Button size="sm" variant="secondary">
retry
</Button>
}
>
the network went away at 62%.
</Alert>
</div>
)
}
// #endregion
export default function AlertDemo() {
return (
<div className="space-y-8">
<Sample name="kinds" glance="wide" title="four kinds" with="alerts">
<Alerts />
</Sample>
<Sample name="glass" title="in glass, over a picture" with="alerts">
<MaterialProvider material="glass">
<div className="rounded-2xl bg-[url(/lightbox/field.jpg)] bg-cover bg-center p-8">
<Alerts />
</div>
</MaterialProvider>
</Sample>
</div>
)
}the source, every file an install writes
"use client"
// A callout in the page's flow: something the reader should know where they are
// reading, said once, never a popup. shadcn's alert, with its kinds as the registry's
// tones:
//
// default a note, in the text's own colour
// success it worked (tone-ok)
// warning look before going on (tone-warn)
// destructive it failed, or it will destroy something (tone-bad)
//
// Each kind wears ONE colour everywhere, alpha the only variable: its icon and title
// in it, its surface a faint wash of it, its edge fainter still; the description stays
// in the text's neutral colour. An icon of the page's own is drawn at the title's
// height (`icon`; each kind has one by default, `icon={null}` for none); `action`
// sits at the end (undo, retry). A callout of the page is read with the page, no live
// region; one that appears because something happened says so (`live`): a
// destructive one at once (role="alert"), the rest when the reader can (role=status).
//
// Two materials (`material`): plain, a wash on the page; glass, a pane of the
// material's glass stained with the kind's colour.
import { CircleAlert, CircleCheck, Info, TriangleAlert } from "lucide-react"
import type * as React from "react"
import { cn } from "@/lib/utils"
import {
type Material,
MaterialProvider,
type Scheme,
useMaterial,
useSurfaceScheme,
} from "@/registry/base-nova/ui/material"
import {
glassStyle,
glassVariants,
schemeOf,
} from "@/registry/base-nova/ui/material-glass"
export type AlertKind = "default" | "success" | "warning" | "destructive"
/** Each kind's colour, as the tokens name it; default is the text's own. */
const INK: Record<AlertKind, string> = {
default: "currentColor",
success: "var(--tone-ok)",
warning: "var(--tone-warn)",
destructive: "var(--tone-bad)",
}
const ICON: Record<AlertKind, React.ReactNode> = {
default: <Info />,
success: <CircleCheck />,
warning: <TriangleAlert />,
destructive: <CircleAlert />,
}
export interface AlertProps extends Omit<React.ComponentProps<"div">, "title"> {
kind?: AlertKind
title?: React.ReactNode
/** The icon at the title's height; each kind has one, `null` for none. */
icon?: React.ReactNode | null
/** At the end: a verb the reader can answer with (undo, retry). */
action?: React.ReactNode
material?: Material
/** The colour scheme it stands in (material): default the nearest provider's. */
scheme?: Scheme
/** It appeared because something happened (a save failed, a retry worked): a
* screen reader says it as it comes, a destructive one at once (`role=alert`),
* the rest when it can (`role=status`). Off, it is a callout of the page, read
* with the page like any paragraph, no live region. */
live?: boolean
}
export function Alert({
kind = "default",
title,
icon,
action,
material,
scheme,
live = false,
className,
style,
children,
ref,
...props
}: AlertProps) {
const m = useMaterial(material)
const [s, here] = useSurfaceScheme<HTMLDivElement>(scheme, ref)
const ink = INK[kind]
const glyph = icon === undefined ? ICON[kind] : icon
return (
<div
ref={here}
data-slot="alert"
data-kind={kind}
data-material={m}
role={live ? (kind === "destructive" ? "alert" : "status") : undefined}
className={cn(
schemeOf(s),
m === "glass"
? [
glassVariants({ shape: "surface" }),
"[--glass-radius:--theme(--radius-2xl)]",
]
: // The wash and the edge are the kind's colour at alpha, the edge at or
// below the wash so the fill carries it.
"rounded-xl border border-(--ag-alert-ink)/12 bg-(--ag-alert-ink)/6",
"flex items-start gap-3 px-4 py-3 text-sm",
className,
)}
style={
{
"--ag-alert-ink": ink,
...(m === "glass" && kind !== "default"
? glassStyle({ tint: ink })
: {}),
...style,
} as React.CSSProperties
}
{...props}
>
{/* What is inside stands on the alert: it wears the alert's scheme, whatever
decided it (a picture under the alert included). */}
<MaterialProvider scheme={s}>
{glyph && (
<span
aria-hidden
data-slot="alert-icon"
className="mt-0.5 grid size-4 shrink-0 place-items-center text-(--ag-alert-ink) [&_svg]:size-4"
>
{glyph}
</span>
)}
<div className="flex min-w-0 flex-1 flex-col gap-0.5">
{title && (
<p
data-slot="alert-title"
className="font-medium text-(--ag-alert-ink) leading-5"
>
{title}
</p>
)}
{children && (
<div
data-slot="alert-description"
className="text-current/70 leading-5"
>
{children}
</div>
)}
</div>
{action && (
<div data-slot="alert-action" className="-my-0.5 shrink-0">
{action}
</div>
)}
</MaterialProvider>
</div>
)
}