Banners, alerts & status
The feedback surfaces — the components that tell the user what happened, what’s missing, what’s loading, and where they are in a flow. Four primitives: Alert (an inline banner with semantic tone + sensible ARIA defaults), EmptyState / ErrorState (the two faces of a region with no content), Skeleton / SkeletonText (loading placeholders) and ProgressSteps (a stepper for multi-stage flows). Every one is theme-aware and pulls from the shared semantic palette (good / warn / bad / accent) — flip the theme in the top bar to confirm each tone re-skins. None of them lean on color alone: each carries a word, an icon or a check so the meaning survives for color-blind users and assistive tech.
- <Alert>
- Inline banner for a single message tied to an action or page state. Four tones (info / success / warning / error). Sets role + aria-live by tone automatically.
- <EmptyState>
- The calm “nothing here yet” state for a region with no data — icon, title, description and an optional action. NOT an error look.
- <ErrorState>
- The failure counterpart to EmptyState — same centered layout, a built-in danger glyph, and an optional retry control to recover.
- <Skeleton> / <SkeletonText>
- Pulsing placeholders shown while content loads. Skeleton is one sized block; SkeletonText stacks N text lines (last shortened).
- <ProgressSteps>
- A numbered stepper for a multi-stage flow. Each step is completed / current / pending; renders as an ordered list with aria-current on the active step.
Alert — tones (title + body)
The four tone keys, each with a bold title line and a body sentence. tone is the only required-ish prop (defaults to info); it sets the fill, border tint and text color together. Reach for the semantic tones only when the message truly carries that state — success after a save, warning for a recoverable problem, error for a failure. info is the neutral, brand-toned default.
Heads up
Your NVDA form was synced from the response sheet 4 minutes ago.
Saved
Your volunteer profile has been updated and is visible to your team leads.
Address incomplete
We couldn’t derive your districts — add a full street address to unlock the Representatives card.
Couldn’t send the invite
The email service is unavailable right now. Try again in a few minutes.
- tone="info"
- bg-accent-soft / border / text-accent — role="status", aria-live="polite" — non-urgent, announced politely.
- tone="success"
- bg-[--color-good-bg] / border good@35% / text-good — role="status", aria-live="polite".
- tone="warning"
- bg-[--color-warn-bg] / border warn@35% / text-warn — role="alert", aria-live="assertive" — interrupts; treated as urgent.
- tone="error"
- bg-[--color-bad-bg] / border bad@35% / text-bad — role="alert", aria-live="assertive".
Alert — body-only & the ARIA contract
The title is just markup you pass as children — an Alert is happy with a single line of body text. What’s NOT optional is the live-region behaviour baked into the component: error and warning render as role=alert / aria-live=assertive (announced immediately), while info and success are role=status / aria-live=polite. Because a live region must already exist in the DOM before the message is inserted to be reliably announced, mount the Alert in a persistent wrapper rather than conditionally on the message — or override role / aria-live at the call site.
- tone (default info)
- keyof tones — info | success | warning | error. Drives fill, border tint and text color from the semantic palette.
- role (auto)
- Computed from tone: error/warning → "alert", info/success → "status". Pass role={undefined} for a purely decorative banner.
- aria-live (auto)
- Paired with role: "alert" → "assertive", "status" → "polite". Override explicitly if you need different urgency.
- children
- The message. Add your own title/body/icon structure as markup — the component only styles the container.
- live-region gotcha
- Keep the Alert mounted in a persistent region; inserting it only when a message exists can skip the announcement.
- …HTMLAttributes
- Spreads onto the <div> — className merges via cn, plus any data-*, id, etc.
EmptyState — icon · title · description · action
The canonical “no data yet” block: a faint centered icon, a title, an optional description, and an optional action. It is deliberately calm and neutral — NOT an error look — for a region that simply has nothing in it yet (an empty team list, no notifications, a fresh search with no matches). The icon is decorative (mark it aria-hidden); the title carries the meaning.
No notifications yet
When a team lead posts an update or invites you to an event, it’ll show up here.
No volunteers match “husky”
Try a broader search, or clear the filters to see everyone.
You’re all caught up
- icon
- Optional ReactNode. Centered in an h-8 w-8 box at text-faint. Decorative — pass aria-hidden on the glyph.
- title
- string (required). Rendered as the primary line (text-sm font-medium, primary text color).
- description
- Optional string. A muted supporting sentence under the title — say what would fill the space, or how to get there.
- action
- Optional ReactNode (typically a Button). Spaced below the copy. Give people the one move that resolves the emptiness.
- vs ErrorState
- Use EmptyState for “nothing here yet”; use ErrorState for “something broke”. They share a layout but read very differently.
ErrorState — title · description · retry
The failure counterpart to EmptyState: the same centered layout, but with a built-in danger glyph (AlertTriangle in text-bad — you don’t pass an icon) and a subtle danger read. title defaults to “Something went wrong”; pass a retry control (usually a Button) to let the user recover. Use it when a fetch or action genuinely failed — not for an empty-but-fine region.
Something went wrong
We couldn’t load your teams. Check your connection and try again.
Couldn’t load representatives
The officials service didn’t respond. This usually clears up on its own.
Something went wrong
- title
- Optional string, defaults to "Something went wrong". Primary text line.
- description
- Optional string. A muted sentence explaining the failure in plain language — and ideally that it may be temporary.
- retry
- Optional ReactNode (a Button). Shown below the copy. Wire it to re-run the failed fetch/action so people can recover in place.
- built-in icon
- AlertTriangle in text-bad, centered above the title (aria-hidden). You do NOT pass an icon — it’s fixed, unlike EmptyState.
Skeleton & SkeletonText — loading placeholders
Pulsing grey placeholders that hold a region’s shape while its content loads, so the layout doesn’t jump when data arrives. Skeleton is one block you size yourself with className (h-* / w-* / rounded-*); SkeletonText stacks N equal text lines and shortens the last one to read as prose. Both are aria-hidden — they convey nothing to assistive tech, so pair the loading region with an aria-busy / status message elsewhere. The pulse is a plain animate-pulse.
- Skeleton className
- Size + shape the block yourself with utilities (h-* / w-* / rounded-*). Base is animate-pulse rounded-md bg-surface-3.
- SkeletonText lines
- number (default 3). Renders that many h-4 lines, space-y-2; the final line is w-2/3 so it reads like the end of a paragraph.
- SkeletonText className
- Merged onto the stack wrapper (e.g. constrain width). The lines themselves are fixed-height h-4 blocks.
- aria-hidden
- Both are aria-hidden — purely visual. Announce the load with aria-busy on the region or a separate status message.
- match the real layout
- Mirror the eventual content’s shape (avatar circle, title line, body lines) so nothing shifts when data swaps in.
ProgressSteps — completed · current · pending
A numbered stepper for a multi-stage flow, rendered as a semantic ordered list. Each step has one of three statuses — completed | current | pending (the exact ProgressStepStatus union) — and the active step carries aria-current=step. A completed step shows a check and its trailing connector turns accent (the trail behind you is “done”); the current step shows its number in an accent circle; pending steps are muted with neutral connectors. Below is a realistic four-step volunteer onboarding flow, shown both horizontally (the default) and vertically.
- Sign up
- Verify email
- Add address
- Join a team
- Sign up
- Verify email
- Add address
- Join a team
- Done
- Doing
- To do
- steps
- ProgressStep[] (required). Each: { label: ReactNode; status; ariaLabel? }. Order defines the run and the numbering.
- status="completed"
- Filled accent circle with a check glyph; the connector that follows it is accent (the trail is “done”). Label in primary text.
- status="current"
- Filled accent circle showing its number; carries aria-current="step". The trailing connector stays neutral. Label in primary text.
- status="pending"
- Muted surface-3 circle with a bordered edge, showing its number; neutral connector. Label in muted text.
- orientation
- "horizontal" (default) lays steps in a row with horizontal connectors; "vertical" stacks them with vertical connectors.
- aria-label
- Names the whole process on the <ol> (default "Progress"). Set it to the flow’s name, e.g. "Volunteer onboarding".
- step ariaLabel
- Per-step accessible name. Falls back to `${label}, ${statusSuffix}` when label is a string; pass it when label is a non-string node.
- connector logic
- The connector after a step is accent only when that step is completed — so the colored trail stops at the current step.
When to use which
These feedback surfaces overlap in spirit but answer different questions. Pick by what the user needs to know right now.
- Alert
- “Here’s what just happened / what to know about this page.” An inline message attached to an action or page state — transient or standing.
- EmptyState
- “There’s nothing here yet, and that’s fine.” For a region with no data — and tell people how to fill it.
- ErrorState
- “We tried and it broke.” For a genuine failure — give a plain reason and a retry. Don’t use it for a merely-empty region.
- Skeleton / SkeletonText
- “It’s coming.” While data loads, hold the shape so the layout doesn’t jump. Replace with content (or Empty/Error) when the request settles.
- ProgressSteps
- “You’re here in the flow.” For a known, ordered multi-step process — onboarding, an application, a checkout.
- never color alone
- Across all of them, meaning rides on words, icons or a check — the hue only reinforces it (for color-blind users and AT).