Alert
Inline contextual feedback for the page the user is on — errors, warnings, confirmations and guidance that live in the content flow. Five intents with derived live-region roles, four variants, link-capable actions in two placements, free-form body content, and a parity skeleton.
Playground
Installation
pnpm add @tessinaui/uiUsage
import { Alert } from "@tessinaui/ui";
<Alert
intent="error"
title="Something went wrong"
description="There was an issue processing your request. Please try again."
action={{ label: "Retry", onClick: retry }}
/>When to use
Alert sits in the middle of the feedback ladder — pick the surface by lifetime and blast radius, not by color:
| Surface | Use for | Lifetime |
|---|---|---|
Toast | Transient confirmation of an action just taken | Seconds, auto-dismisses |
Alert | State of THIS page or section — failures, pending steps, guidance | Until resolved or dismissed |
Banner | Site-wide or promotional messaging (upsell, announcements) | Page-level chrome |
AlertDialog | Blocking decisions that must interrupt | Until answered |
- Route success to a toast unless it is delayed, persistent, or carries a CTA.
- Place the alert next to what it describes; avoid more than one per page — combine messages instead.
- Field-level validation belongs to
Fielderrors; the alert is the form-LEVEL summary (see Error summary).
Examples
Default
A basic alert with a title and supporting description.
Intents
Five semantic intents — none, error, warning, success, and info — each with its own colour, icon and derived live-region role.
Variants
Four visual fill styles — solid, soft, outline, and ghost. Solid is the high-emphasis tier (connectivity bars, must-see failures); soft is the default.
Sizes
Three sizes — sm, md, and lg — control text size and padding.
Body only
No title: the icon rides the first line of the message — or drop the icon for a text-only strip beside a destructive control. A bold run-in lead lives in children.
With actions
Trailing placement (default): actions sit beside the message, vertically centred, with the dismiss control alone in the corner. When the message needs the room — a long title, a narrow column — they drop below it and line up with the text, rather than squeezing it (Fluent's MessageBar reflow).
Actions below
actionsPlacement="below" keeps the buttons in the text flow at every width.
Links
Inline links inside the body, and href actions that render real anchors dressed as buttons.
Error summary
The post-submit pattern: titleAs="h2" for a real heading, list items linking to the failing fields, and autoFocus to move keyboard focus to the summary — the one sanctioned exception to “an alert never moves focus”.
Dismissible
onClose renders the ×; you own the state. Offer dismissal on informational and transient notices — never on destructive warnings or anything the user must resolve.
Live region roles
Roles derive from intent: error/warning announce assertively, the rest politely. role="none" opts a static callout out of the announcement stream entirely.
Acknowledgeable notice
Body copy with quiet text actions below — “Got it” / “Don't show again”. Also the recommended shape for AI-provenance notes when one must be boxed at all.
Rounded
The §2 cascade: shell, action and dismiss step together from none to full (the panel capsule cap).
Curving your own content
Content you put inside — a thumbnail beside the message — should curve like the chrome around it, or it reads as pasted on. The alert panel publishes its inner radius as a CSS variable:
<img className="rounded-[var(--alert-item-radius)]" />It tracks whatever the component is actually doing: the explicit step when rounded is set (8px at md), and 0 at rounded="none" — so your content squares off exactly when the container does.
RTL
Logical properties end-to-end — icon, text, actions and dismiss all mirror under dir="rtl".
Loading
AlertSkeleton is the same box as the Alert — size, rounded, chrome and actions placement all mirror, so nothing jumps when content arrives.
Variants
| Variant | Description |
|---|---|
solid | Filled background using the full intent colour. High emphasis — connectivity bars, must-see failures. |
soft | Light tinted background (bg-error-light) with semantic icon colour. Default. |
outline | Coloured border + very subtle tint. |
ghost | No background or border. Icon still carries the semantic colour. Minimal emphasis. |
Intents
| Intent | Icon | Derived role | Use case |
|---|---|---|---|
none | Info | status | Neutral informational message |
error | CircleX | alert | Failure, validation summary |
warning | TriangleAlert | alert | Needs attention before it becomes an error |
success | CircleCheck | status | Persistent confirmation (transient success → toast) |
info | Info | status | Guidance, context |
Props
Alert
The root also forwards every standard div attribute (id, data-*,
aria-*, style, event handlers) — title and role are the two it
repurposes.
| Prop | Type | Default | Description |
|---|---|---|---|
intent | "none" | "error" | "warning" | "success" | "info" | "none" | Semantic colour, default icon, derived role |
variant | "solid" | "soft" | "outline" | "ghost" | "soft" | Visual fill style |
size | "sm" | "md" | "lg" | "md" | Text size and padding |
rounded | "none" | "sm" | "md" | "lg" | "xl" | "full" | "md" | Border radius — nested controls follow (§2). full is the panel capsule cap |
width | "narrow" | "default" | "wide" | "default" | Max-width constraint — narrow (sm), default (full), wide (4xl) |
title | ReactNode | — | Heading text |
titleAs | "p" | "h2" | "h3" | "h4" | "h5" | "h6" | "p" | Render the title as a real heading when the alert opens a section |
description | ReactNode | — | Supporting body text |
children | ReactNode | — | Free-form body below the description — lists, links, paragraphs |
icon | ReactNode | Intent default | Custom leading icon (skips the optical nudge) |
showIcon | boolean | true | Show or hide the leading icon |
onClose | () => void | — | Renders the dismiss (×) button; visibility state is yours |
closeLabel | string | "Dismiss" (+ title) | Accessible label for the × — localize it here |
action | AlertAction | — | Primary CTA |
secondaryAction | AlertAction | — | Secondary CTA beside the primary |
actionsPlacement | "trailing" | "below" | "trailing" | Trailing = beside the message, centred, dropping below it when the text needs the width; below = in the text flow always |
shadow | boolean | false | Drop shadow — floating callout style |
dir | "ltr" | "rtl" | "ltr" | Text direction |
role | "alert" | "status" | "none" | derived | Live-region role — derived from intent when omitted |
autoFocus | boolean | false | Focus the alert on mount — error-summary pattern only |
announceOnMount | boolean | false | Make a polite (status) alert speak when inserted — mounts the region empty, then fills it. Ignored for alert and none |
className | string | — | Extra classes on the root element |
AlertAction
| Field | Type | Description |
|---|---|---|
label | string | Button text |
onClick | () => void | Click handler |
href | string | Renders the action as a real link (<a>), Button-dressed |
target / rel | string | Link attributes — _blank defaults rel to noopener noreferrer |
variant | ButtonProps["variant"] | Override button fill style |
intent | ButtonProps["intent"] | Override button intent colour |
rounded | ButtonProps["rounded"] | Override button radius (defaults to the alert's rounded) |
disabled | boolean | Disabled state |
loading | boolean | Loading spinner state |
leadingIcon / trailingIcon | ReactNode | Icons around the label |
AlertSkeleton
Mirrors the Alert's box (§2b): variant, intent, size, rounded,
width, shadow, dir, plus showIcon, showTitle, lines, showAction,
showSecondaryAction, actionsPlacement, closeable.
Accessibility
Pattern: WAI-ARIA Alert.
- Roles derive from severity —
error/warningrenderrole="alert"(assertive),success/info/nonerenderrole="status"(polite), androleoverrides both. Content rendered with the page announces nothing, whatever the role (userole="none"to opt static callouts out entirely). - An inserted
role="alert"is announced; an insertedrole="status"may not be. Verified on 2026-09-07 with VoiceOver in both Safari and Chrome: injecting an error alert spoke immediately, while injecting a success alert stayed silent in both browsers. This is the live-region rule, not a bug in the component — a polite region is expected to be in the DOM before its content changes, and onlyalertis special-cased to speak on insertion. If a non-urgent message must be heard the moment it appears, passannounceOnMount— the alert mounts its region empty and fills it a beat later, so the insertion reads as the content change a polite region needs. Verified with VoiceOver in Safari: the same success alert is silent without the prop and spoken with it. The prop is opt-in because doing it to every status alert would make the ones already on the page speak at load. - Focus is never moved by an alert. The one sanctioned exception is
autoFocusfor the post-submit error summary (tabIndex={-1}+ visible focus ring), pairing the summary with per-field errors. - The leading icon is decorative (
aria-hidden); meaning never rides on color alone — the intent word belongs in the text. - The dismiss button's name comes from
closeLabel(the default is "Dismiss", enriched with the title when it is a plain string). Actions read as buttons,hrefactions as links. - Content wraps — titles and bodies are never truncated mid-thought
(
break-wordshandles long tokens/URLs). - Touch: every control clears 44×44px on phones via the Button/IconButton floors; actions drop below the message whenever it needs the width, so they never compress the text on a narrow column.
Keyboard
| Key | Behaviour |
|---|---|
Tab / Shift+Tab | Moves through action(s), then the dismiss button — natural order, no trap |
Enter / Space | Activates the focused action or dismiss |
Esc | Nothing — the alert is not modal (that's AlertDialog) |
AT matrix
| AT | Status |
|---|---|
| VoiceOver + Chrome (macOS) | Passed 2026-09-07 — role announced as "alert", dismiss read as "Dismiss" plus the alert title, titleAs="h2" read as "heading level 2", autoFocus summary read in full. Caveat above: an inserted polite region did not speak |
| VoiceOver + Safari (macOS) | Passed 2026-09-07 — an inserted error alert spoke immediately; the dismiss read as "Dismiss" plus the alert title; the polite case below reproduced |
| NVDA + Chrome (Windows) | UNTESTED — no Windows host available |
| JAWS | UNTESTED |
Component stays beta until one VoiceOver and one NVDA pass are on record
(QA flow §5.6).