← ui

alert

ui

a callout in the page's flow: success, warning or error, with an action

npx shadcn add @ag/alert
Already installed? Preview an update
npx shadcn add @ag/alert --dry-run

Your 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; live makes one that appears because something happened say so (role=alert when destructive, role=status otherwise). Migration: add live where an alert is shown after an action.
Updated 2026-10-11 · release notes

also called banner

shelf controls

uses material

used by components

the demo above, verbatim

alert.demo.tsx
"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

alert.tsx
"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>
  )
}