Skip to content

Alert

An inline message about the state of a page or section that stays until resolved.

Status
Beta
Category
Feedback
Adoption
Not used yet
import { Alert } from "@oration/canon/components/alert";
packages/canon/src/components/alert.tsx
Friday's payment run needs approval
212 invoices to 48 suppliers, $1,284,310.00 in total. Approve by Thursday at 5:00 PM CT so suppliers are paid on time.
import { Alert, AlertAction, AlertDescription, AlertTitle } from "@oration/canon/components/alert";import { Button } from "@oration/canon/components/button";import { toast } from "@oration/canon/components/toast";import { LandmarkIcon } from "lucide-react";export function Hero() {    return (        <div className="w-full max-w-xl">            <Alert role="status">                <LandmarkIcon aria-hidden="true" />                <AlertTitle>Friday's payment run needs approval</AlertTitle>                <AlertDescription>                    212 invoices to 48 suppliers, $1,284,310.00 in total.                    Approve by Thursday at 5:00 PM CT so suppliers are paid on                    time.                </AlertDescription>                <AlertAction>                    <Button                        type="button"                        variant="outline"                        size="xs"                        onClick={() =>                            toast.add({                                title: "Opened the Oct 2 payment run",                                description:                                    "212 invoices are waiting for your approval.",                            })                        }                    >                        Review                    </Button>                </AlertAction>            </Alert>        </div>    );}

Usage#

Alert is an inline message about the state of a page, section or field: a W-9 that was rejected, a channel that no longer exists, a payment run waiting on approval. It sits in the layout beside the thing it describes and stays there until the condition is resolved. The common mistake is using it as feedback for an action that just finished; a save, a send or a delete answers with a toast, and the alert is kept for conditions someone still has to act on.

When to use

  • For a condition that blocks or changes what happens next: Payments to Halcyon pause until a corrected W-9 arrives.
  • Beside a field whose saved value no longer works, such as a Slack channel that was archived.
  • For a failed step in a run log, with the fix as an action: Edit step, Retry.
  • At the top of a section when something needs a decision before a deadline: Friday's payment run needs approval.
  • When the message must stay visible until someone resolves it, not vanish after five seconds.

When not to use

  • To confirm an action that finished, such as Remittance sent. Answer with a toast, with Undo when it can be reversed. Use Toast
  • For a decision that must be made before continuing, such as deleting a supplier. Use Confirm dialog
  • To explain what a page is for. Education belongs in a dismissible intro. Use Page intro
  • For a validation error on one input. Put the error under the field. Use Field
  • For a failed read of a whole region. The component owns its error state, with Retry. Use Data state
  • For unsaved changes. The save bar owns that message. Use Save bar

The Label-Beside-Color Rule

Status is never color alone. A destructive alert is red text, and the title still says what broke in words, so it reads the same in grayscale.

Say what happened and what to do

Every alert names the condition in its title and the fix in its description or action. It never blames the reader and never says Something went wrong.

Anatomy#

Halcyon's W-9 expires Oct 15
Request a new one before the next payment run.
  1. Container. A full-width grid with 10px corners, a 1px Hairline border on Card White, and 10px by 8px padding.
  2. Icon. Optional. A direct child svg, 16px, spanning both text rows in the current text color. Its presence switches the grid to two columns.
  3. Title. 14px at weight 500. One line that names the condition: Halcyon's W-9 was rejected.
  4. Description. Optional. 14px Slate Meta: the consequence and the fix. Links inside it are underlined.
  5. Action. Optional AlertAction, pinned 8px from the top and right corners. The container reserves 72px on the right for it.

Examples#

Variants

Default for a condition that needs attention, destructive for something that failed or is blocked.

Bank details changed for Northwind Freight
Remittances to Northwind Freight hold for 24 hours while Priya Raman verifies the new account.
import { Alert, AlertDescription, AlertTitle } from "@oration/canon/components/alert";import { CircleAlertIcon, LandmarkIcon } from "lucide-react";export function Variants() {    return (        <div className="flex w-full max-w-xl flex-col gap-3">            <Alert role="status">                <LandmarkIcon aria-hidden="true" />                <AlertTitle>                    Bank details changed for Northwind Freight                </AlertTitle>                <AlertDescription>                    Remittances to Northwind Freight hold for 24 hours while                    Priya Raman verifies the new account.                </AlertDescription>            </Alert>            <Alert variant="destructive">                <CircleAlertIcon aria-hidden="true" />                <AlertTitle>Halcyon's W-9 was rejected</AlertTitle>                <AlertDescription>                    The TIN doesn't match IRS records. Payments to Halcyon pause                    until a corrected W-9 arrives.                </AlertDescription>            </Alert>        </div>    );}

Title, description and icon

A title alone for a short condition, a description when the reader needs the consequence and the fix, and no icon when the alert sits inside a dense panel.

3 invoices are missing a PO number
3 invoices are missing a PO number
They stay out of Friday's run until someone adds one. Orchard Street sent two of them.
3 invoices are missing a PO number
Without an icon the title and description sit flush left.
import { Alert, AlertDescription, AlertTitle } from "@oration/canon/components/alert";import { TriangleAlertIcon } from "lucide-react";export function Content() {    return (        <div className="flex w-full max-w-xl flex-col gap-3">            <Alert role="status">                <TriangleAlertIcon aria-hidden="true" />                <AlertTitle>3 invoices are missing a PO number</AlertTitle>            </Alert>            <Alert role="status">                <TriangleAlertIcon aria-hidden="true" />                <AlertTitle>3 invoices are missing a PO number</AlertTitle>                <AlertDescription>                    They stay out of Friday's run until someone adds one.                    Orchard Street sent two of them.                </AlertDescription>            </Alert>            <Alert role="status">                <AlertTitle>3 invoices are missing a PO number</AlertTitle>                <AlertDescription>                    Without an icon the title and description sit flush left.                </AlertDescription>            </Alert>        </div>    );}

With actions

One short action goes in AlertAction. Two or more go in the description as a row of extra-small buttons, as the workflow run log does.

Halcyon's W-9 expires Oct 15
Request a new one before the next payment run.
import { Alert, AlertAction, AlertDescription, AlertTitle } from "@oration/canon/components/alert";import { Button } from "@oration/canon/components/button";import { toast } from "@oration/canon/components/toast";import {  CircleAlertIcon,  FileWarningIcon,  PencilLineIcon,  RotateCcwIcon,} from "lucide-react";export function WithActions() {    return (        <div className="flex w-full max-w-xl flex-col gap-3">            <Alert role="status">                <FileWarningIcon aria-hidden="true" />                <AlertTitle>Halcyon's W-9 expires Oct 15</AlertTitle>                <AlertDescription>                    Request a new one before the next payment run.                </AlertDescription>                <AlertAction>                    <Button                        type="button"                        variant="outline"                        size="xs"                        onClick={() =>                            toast.add({                                type: "success",                                title: "W-9 requested from Halcyon",                                description:                                    "Wen Zhou gets a copy of the request.",                            })                        }                    >                        Request                    </Button>                </AlertAction>            </Alert>            <Alert variant="destructive">                <CircleAlertIcon aria-hidden="true" />                <AlertTitle>Couldn't post to #ap-payments</AlertTitle>                <AlertDescription className="flex flex-wrap items-center gap-2">                    <span>                        The channel was archived. Pick another one, then retry.                    </span>                    <span className="flex gap-1.5">                        <Button                            type="button"                            variant="outline"                            size="xs"                            onClick={() =>                                toast.add({                                    title: "Opened the Notify finance step",                                })                            }                        >                            <PencilLineIcon                                data-icon="inline-start"                                aria-hidden="true"                            />                            Edit step                        </Button>                        <Button                            type="button"                            variant="ghost"                            size="xs"                            onClick={() =>                                toast.add({                                    type: "info",                                    title: "Retrying the Notify finance step",                                })                            }                        >                            <RotateCcwIcon                                data-icon="inline-start"                                aria-hidden="true"                            />                            Retry                        </Button>                    </span>                </AlertDescription>            </Alert>        </div>    );}

Resolving in place

An alert stays until its condition clears. When it does, swap it for the resolved message in the same spot so the layout holds still.

import { Alert, AlertAction, AlertDescription, AlertTitle } from "@oration/canon/components/alert";import { Button } from "@oration/canon/components/button";import { CheckIcon, CircleAlertIcon } from "lucide-react";import * as React from "react";export function ResolvesInPlace() {    const [requested, setRequested] = React.useState(false);    return (        <div className="w-full max-w-xl">            {requested ? (                <Alert>                    <CheckIcon aria-hidden="true" />                    <AlertTitle>New W-9 requested from Halcyon</AlertTitle>                    <AlertDescription>                        Payments stay paused until it arrives. We'll email Maya                        Okafor when it does.                    </AlertDescription>                    <AlertAction>                        <Button                            type="button"                            variant="ghost"                            size="xs"                            onClick={() => setRequested(false)}                        >                            Undo                        </Button>                    </AlertAction>                </Alert>            ) : (                <Alert variant="destructive">                    <CircleAlertIcon aria-hidden="true" />                    <AlertTitle>Halcyon's W-9 was rejected</AlertTitle>                    <AlertDescription>                        The TIN doesn't match IRS records. Payments pause until                        a corrected W-9 arrives.                    </AlertDescription>                    <AlertAction>                        <Button                            type="button"                            variant="outline"                            size="xs"                            onClick={() => setRequested(true)}                        >                            Request                        </Button>                    </AlertAction>                </Alert>            )}        </div>    );}

Under a field

In the workflow inspector, a saved value that no longer works gets a destructive alert under its control. Pick another channel and it goes away.

Notify finance

Posts a message when a payment run finishes.

import { Alert, AlertDescription, AlertTitle } from "@oration/canon/components/alert";import { Label } from "@oration/canon/components/label";import { NativeSelect, NativeSelectOption } from "@oration/canon/components/native-select";import { TriangleAlertIcon } from "lucide-react";import * as React from "react";export function InAField() {    const id = React.useId();    const [channel, setChannel] = React.useState("ap-payments");    const archived = channel === "ap-payments";    return (        <div className="flex w-full max-w-sm flex-col gap-4 rounded-xl bg-card p-4 shadow-border">            <div className="flex flex-col gap-1">                <p className="text-sm font-semibold">Notify finance</p>                <p className="text-13 text-muted-foreground">                    Posts a message when a payment run finishes.                </p>            </div>            <div className="flex flex-col gap-1.5">                <Label htmlFor={id}>Channel</Label>                <NativeSelect                    id={id}                    value={channel}                    onChange={(event) => setChannel(event.target.value)}                    aria-invalid={archived || undefined}                    className="w-full"                >                    <NativeSelectOption value="ap-payments">                        #ap-payments (archived)                    </NativeSelectOption>                    <NativeSelectOption value="ap-exceptions">                        #ap-exceptions                    </NativeSelectOption>                    <NativeSelectOption value="finance-ops">                        #finance-ops                    </NativeSelectOption>                </NativeSelect>                {archived ? (                    <Alert variant="destructive" className="mt-1">                        <TriangleAlertIcon aria-hidden="true" />                        <AlertTitle>#ap-payments was archived</AlertTitle>                        <AlertDescription>                            Posts to it fail. Pick another channel, like                            #ap-exceptions.                        </AlertDescription>                    </Alert>                ) : null}            </div>        </div>    );}

States#

States
StateTreatment
DefaultGraphite Ink title and Slate Meta description on Card White. For notices and conditions that need attention but aren't broken.
DestructiveThe title and icon turn Signal Red and the description red at 90%. For something that failed or is blocked.
With iconA direct svg child switches to a two-column grid; the title and description start in column two.
With actionAn AlertAction child adds 72px of right padding so the title never runs under the button.
Link hoverLinks in the title and description are underlined at a 3px offset and turn Graphite Ink on hover.
ResolvedWhen the condition clears, remove the alert or swap it for the resolved message in place. Don't leave a stale alert on screen.

Behavior#

  • Alert is static markup. It has no open, close or dismiss behavior of its own; it renders while your condition is true.
  • role="alert" is set by default, so an alert that appears in response to something (a failed retry, a changed field) is announced right away.
  • For a notice that is simply present when the page loads, pass role="status"; your role overrides the default because props spread last.
  • The icon must be a direct child svg for the two-column layout; wrapping it in a span drops the grid.
  • For one action, use AlertAction and keep it short (about 64px). For two or more, put them in the description as a row of xs buttons, as the run log does.
  • No animation. If the alert replaces another, swap it in place so the layout doesn't jump.

Do and don't#

Do. Name the condition in the title and the fix in the description: what was rejected, why, and what to do.
Don't. Write Error and Something went wrong. The reader learns nothing and has nowhere to go.
Bank details pending verification
Remittances to Northwind Freight hold until Priya Raman confirms.
Do. Use an alert for a condition that persists until someone resolves it.
Don't. Confirm a finished action with an alert. Settings saved belongs in a toast.
Do. Summarize related failures in one alert and list what's affected.
Don't. Stack an alert per item. Three red boxes read as three problems and push the page down.

Content#

  • Title: the condition as a short statement, sentence case, no trailing period: Halcyon's W-9 was rejected.
  • Description: one or two sentences with the consequence and the fix: Payments pause until a corrected W-9 arrives.
  • Name the object: the supplier, the run, the channel. Avoid this item and the record.
  • Give dates and amounts exactly, in tabular figures: Approve by Thursday at 5:00 PM CT, $1,284,310.00.
  • Action labels start with a verb and name the fix: Request W-9, Edit step, Retry. Not OK or Learn more.
  • Calm and plain, never blaming: The TIN doesn't match IRS records, not You entered an invalid TIN.

Accessibility#

  • Alert renders role="alert", an assertive live region. Keep it for alerts that appear after something happens; pass role="status" for notices that are already there on load.
  • Mark the icon aria-hidden="true"; the title carries the meaning.
  • Destructive alerts are red text on Card White. The title says what failed, so the meaning doesn't depend on color.
  • Buttons in AlertAction or the description need visible labels that name the fix. Icon-only actions need an aria-label and a tooltip.
  • When an alert sits under a field, also set aria-invalid on the control so the field itself reports the problem.
  • Don't move focus to an alert. The live region announces it; focus stays where the person was working.

Design tokens#

Design tokens
TokenUsed for
--cardBackground of both variants
--card-foregroundTitle and icon color, default
--borderThe 1px container border
--muted-foregroundDescription text, default
--destructiveTitle, icon and description (at 90%), destructive
--radius-lg10px corners

API reference#

Alert

The container. Sets role="alert" and data-slot="alert"; an svg child becomes the icon column.

Other props spread onto <div>.

Props of Alert
PropTypeDefaultDescription
variant"default" | "destructive""default"destructive turns the title, icon and description red.
roleAriaRole"alert"Pass "status" for a notice that is present on load and not urgent.
classNamestringNo defaultMerged after the variant classes.

AlertTitle

The one-line condition. Starts in column two when an icon is present.

Other props spread onto <div>.

Props of AlertTitle
PropTypeDefaultDescription
classNamestringNo defaultMerged after the defaults.

AlertDescription

The consequence and the fix, in Slate Meta. Paragraphs inside it are spaced 16px apart.

Other props spread onto <div>.

Props of AlertDescription
PropTypeDefaultDescription
classNamestringNo defaultMerged after the defaults; add flex flex-wrap gap-2 to lay out inline buttons.

AlertAction

Holds one short action, absolutely positioned 8px from the top right.

Other props spread onto <div>.

Props of AlertAction
PropTypeDefaultDescription
classNamestringNo defaultMerged after absolute top-2 right-2.

Known gaps#

Where the implementation and the system disagree today. Follow the system, not the gap.

The container draws its edge with a 1px CSS border on Card White. DESIGN.md keeps CSS borders for structural dividers; an inline notice would follow the Tint Well Rule (Well Gray at 70%) instead.

There are only default and destructive variants. DESIGN.md defines Caution Amber for warnings and Note Blue for informational notices, and the product hand-rolls warning notices as bg-warning/10 wells in several places (public keys settings, the procedure publish dialog) rather than using Alert.

role="alert" is hard-coded as the default, so every alert is assertive, including static notices present on page load. Pass role="status" for those.

AlertAction reserves a fixed 72px (pr-18) on the right. An action wider than about 64px overlaps the title; keep it to one short button or move actions into the description.

Alert is used in two places in the app today (the workflow inspector and the run log), both destructive.