Feedback
Alert and Callout
One surface, two intents. An Alert reports a status about something that happened; a Callout explains something persistent. Neither is announced to assistive technology unless you ask for it.
Examples
Five tones
A surface, a semantic edge and a semantic icon — never a flooded panel. These blocks sit beside tables and charts, and a wall of colour destroys the hierarchy.
Nothing to report
Backfill queued
Validation passed
Delayed data
Ingestion failed
<Alert tone="neutral" title="Nothing to report" description="All jobs finished in their window." />
<Alert tone="info" title="Backfill queued" description="1,284 bars will be ingested." />
<Alert tone="success" title="Validation passed" description="Seven analytics domains matched." />
<Alert tone="warning" title="Delayed data" description="Yahoo returned end-of-day prices." />
<Alert tone="danger" title="Ingestion failed" description="Provider returned 503." />One surface, two intents
`emphasis` is the only difference between the two exports: an Alert is a status about something that happened, a Callout is context that stays. They look alike on purpose — the difference is what the reader is meant to do about it.
Alert · a status
Warm-up period
Callout · persistent context
This benchmark comes from an external provider
{/* A status message: it happened, or it needs attention. */}
<Alert tone="warning" title="Warm-up period" description="Sharpe is not meaningful before 60 bars." />
{/* The same surface, declared as persistent context. */}
<Callout tone="info" title="Third-party data" description="This panel loads resources from TradingView." />Composing the parts
`title` and `description` are shorthand for the common case; the parts compose the same block when it has to hold a link, a list or a custom mark. `icon={null}` drops the mark without dropping the tone.
Rebalance in progress
Signed with your own key
<Alert tone="info" icon={null}>
<AlertTitle>Rebalance in progress</AlertTitle>
<AlertDescription>
Orders are queued against the <a href="/docs/api">execution policy</a>.
</AlertDescription>
<AlertActions>
<Button size="sm" variant="secondary">View orders</Button>
</AlertActions>
</Alert>Actions and dismissal
Both are opt-in. The component never persists a dismissal, and it never hides an action behind the close button — a reader who dismisses twice should not lose their way back.
Market widget unavailable
What the close button means
A dismissal is a UI event the caller hears about — persist it, log it, or reopen the alert. The component itself keeps no state, so an alert that returns on the next render is a decision the product made.
External resources
<Alert tone="danger" title="Widget unavailable"
actions={<Button size="sm">Retry</Button>}
onDismiss={() => setDismissed(true)} />When it is announced
`live` is the whole accessibility decision, and `off` is the default: a block that is present when the page loads is already part of the page, and announcing it as an interruption is worse than saying nothing. Press the button — the alert that arrives afterwards announces itself once.
live="polite" → role="status"; live="assertive" → role="alert"; off → no role.
Nothing is announced yet — this text is the page, not a message.
const [warned, setWarned] = useState(false);
<Button onClick={() => setWarned(true)}>Break the connection</Button>
{warned ? (
<Alert
tone="danger"
live="polite"
title="Stream disconnected"
description="Reconnect from the toolbar when you are ready."
/>
) : null}Next to a form
A page-level Alert and a form's own error summary answer different questions; they can sit together without repeating each other. The field keeps its own error — that is where the reader is looking.
<Form errors={errors} onSubmit={submit}>
<Alert tone="info" title="Server-side validation also runs on submit"
description="Client rules are a courtesy; the dataset is validated again." />
<FormErrorSummary />
<Field label="Dataset name" required description="Lowercase, no spaces.">
<Input value={dataset} onChange={change} required pattern="[a-z0-9_]+" />
</Field>
<Button type="submit">Save dataset</Button>
</Form>Usage
import { Alert, Callout } from "@luc/ui/alert";<Alert
tone="warning"
title="Delayed data"
description="Yahoo returned end-of-day prices for the last two sessions."
/>`Callout` takes the same props with the persistent intent; reach for it when the message is context rather than a status.
API
Read from the published TypeScript API, so a prop that is not listed here does not exist and a default that is listed is the one the compiler sees.
`Alert` and `Callout` are one implementation with two intents, and `AlertTitle`, `AlertDescription` and `AlertActions` are the parts that `title`, `description` and `actions` are shorthand for. The two public components are open above; the parts are folded underneath.
| Export | Kind | Signature |
|---|---|---|
Alert | 9 documented props | ({ className, tone, emphasis, title, description, icon, actions, onDismiss, dismissLabel, live, children, ...props }: AlertProps) => import("@types/react").JSX.Element |
AlertActions | export | ({ className, ...props }: AlertActionsProps) => import("@types/react").JSX.Element |
AlertDescription | export | ({ className, ...props }: AlertDescriptionProps) => import("@types/react").JSX.Element |
AlertTitle | export | ({ className, ...props }: AlertTitleProps) => import("@types/react").JSX.Element |
Callout | 9 documented props | ({ emphasis, live, ...props }: AlertProps) => import("@types/react").JSX.Element |
Alert props
Type: AlertProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
actions? | ReactNode | — | |
description? | ReactNode | — | |
dismissLabel? | string | undefined | — | |
emphasis? | "alert" | "quiet" | undefined | — | `alert` (default) is the stronger surface for a status message; `quiet` is the persistent Callout treatment. |
icon? | ReactNode | — | Override the tone icon; pass `null` for a text-only block. |
live? | AlertLive | undefined | — | |
onDismiss? | (() => void) | undefined | — | Renders a dismiss control. Persistence is the consumer's job. |
title? | ReactNode | — | |
tone? | AlertTone | undefined | — |
Callout props
Type: AlertProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
actions? | ReactNode | — | |
description? | ReactNode | — | |
dismissLabel? | string | undefined | — | |
emphasis? | "alert" | "quiet" | undefined | — | `alert` (default) is the stronger surface for a status message; `quiet` is the persistent Callout treatment. |
icon? | ReactNode | — | Override the tone icon; pass `null` for a text-only block. |
live? | AlertLive | undefined | — | |
onDismiss? | (() => void) | undefined | — | Renders a dismiss control. Persistence is the consumer's job. |
title? | ReactNode | — | |
tone? | AlertTone | undefined | — |
Guidelines
Do
- Use an Alert for a status the reader may need to act on, and a Callout for context that stays true while the page is open.
- Say what happened and what the reader can do; a tone without a next step is decoration.
- Keep one page-level Alert per region — two competing messages is a hierarchy that has stopped working.
Don't
- Do not force `live` on content that is present when the page loads: it is already part of the page, and announcing it interrupts the reader.
- Do not report a field's own validation error in an Alert — that error belongs to the field.
- Do not use an Alert for a transient confirmation; that is a toast, and it should not occupy layout.
Accessibility
`live` is the only thing that decides whether a screen reader is interrupted: `off` (the default) renders no role, `polite` renders `role="status"`, and `assertive` renders `role="alert"`. The dismiss control is a real button with a name the caller can localise (`dismissLabel`), and the tone is carried by an edge and an icon as well as by colour, so the state survives a monochrome rendering.
Related