Alert
An inline message about the state of a page or section that stays until resolved.
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
Say what happened and what to do
Anatomy#
- Container. A full-width grid with 10px corners, a 1px Hairline border on Card White, and 10px by 8px padding.
- Icon. Optional. A direct child
svg, 16px, spanning both text rows in the current text color. Its presence switches the grid to two columns. - Title. 14px at weight 500. One line that names the condition: Halcyon's W-9 was rejected.
- Description. Optional. 14px Slate Meta: the consequence and the fix. Links inside it are underlined.
- 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.
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.
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.
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#
| State | Treatment |
|---|---|
| Default | Graphite Ink title and Slate Meta description on Card White. For notices and conditions that need attention but aren't broken. |
| Destructive | The title and icon turn Signal Red and the description red at 90%. For something that failed or is blocked. |
| With icon | A direct svg child switches to a two-column grid; the title and description start in column two. |
| With action | An AlertAction child adds 72px of right padding so the title never runs under the button. |
| Link hover | Links in the title and description are underlined at a 3px offset and turn Graphite Ink on hover. |
| Resolved | When 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"; yourroleoverrides the default because props spread last. - The icon must be a direct child
svgfor the two-column layout; wrapping it in a span drops the grid. - For one action, use
AlertActionand keep it short (about 64px). For two or more, put them in the description as a row ofxsbuttons, 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#
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; passrole="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
AlertActionor the description need visible labels that name the fix. Icon-only actions need anaria-labeland a tooltip. - When an alert sits under a field, also set
aria-invalidon 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#
| Token | Used for |
|---|---|
--card | Background of both variants |
--card-foreground | Title and icon color, default |
--border | The 1px container border |
--muted-foreground | Description text, default |
--destructive | Title, icon and description (at 90%), destructive |
--radius-lg | 10px 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>.
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "default" | "destructive" | "default" | destructive turns the title, icon and description red. |
role | AriaRole | "alert" | Pass "status" for a notice that is present on load and not urgent. |
className | string | No default | Merged after the variant classes. |
AlertTitle
The one-line condition. Starts in column two when an icon is present.
Other props spread onto <div>.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | No default | Merged after the defaults. |
AlertDescription
The consequence and the fix, in Slate Meta. Paragraphs inside it are spaced 16px apart.
Other props spread onto <div>.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | No default | Merged 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>.
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | No default | Merged 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.