A wide, tone-coloured strip pinned to the top of a page or region that communicates a durable, page-level condition: a system incident, a maintenance window, a degraded-mode notice, or a successful bulk action. Banner persists until the user dismisses it or the condition resolves. Error and danger tones announce assertively (role=alert); calmer tones announce politely (role=status). For a message that lives beside the content it qualifies use Callout; for transient action feedback use Toast.
Readiness
complete
Preview
live
Props
3
Examples
4
Code
implementation mapped
When to use
System-wide incident or degraded-mode notice ('Search is running slow')
Scheduled-maintenance announcement at the top of the workspace
Confirmation of a page-level bulk action ('14 records imported')
Account / billing state that gates the whole surface ('Trial ends in 3 days')
When not to use
Transient feedback after a single action — use Toast
A note attached to specific content in document flow — use Callout
A validation error bound to one form field — use the field's error slot
A blocking decision that needs a response — use a Dialog
Accessibility
Role: alert
No keyboard shortcuts
Screen reader: Error and danger tones use role=alert so assistive tech interrupts and announces immediately; info / success / warning use role=status (announced politely at the next pause). The leading icon is decorative (aria-hidden) — the tone meaning lives in the message text. The dismiss control is a real button with an explicit aria-label.
Notes: Never rely on tone colour alone. Phrase the message so it carries the severity in words ('Error: upload failed' rather than a red strip saying 'upload failed').
Props
Name
Type
Default
Description
tone
enum: info | success | warning | error | danger
info
icon
string
—
Optional decorative leading icon key (e.g. info, warning, error).
dismissible
boolean
false
Render a dismiss control. Dismissal is local component state.
Examples
Incident
A degraded-mode incident notice at the top of the workspace.
Search is running slower than usual while we recover an index. Results may be incomplete.
YAML
type: banner
props:
tone: warning
icon: warning
dismissible: true
slots:
default: Search is running slower than usual while we recover an index. Results may be incomplete.
Inline contextual guidance inside document flow. Callout is for durable explanatory notes, warnings, and implementation gotchas that belong beside the content they qualify — it does not interrupt the reading position the way a viewport-pinned Banner does. It uses role=note so assistive tech treats it as supplementary rather than an interruption. For a page-level announcement use Banner; for transient feedback after an action use Toast.
Readiness
complete
Preview
live
Props
3
Examples
4
Code
implementation mapped
When to use
Notes or warnings inside generated docs and guides
Contextual implementation guidance that should not interrupt the workflow
Markdown blockquote-style asides that should render as first-class UI
A 'gotcha' beside a configuration field that needs explaining
When not to use
Page-level incidents or system announcements - use Banner
Transient feedback after an action - use Toast
Validation errors bound to a form field - use the field's error slot
A blocking decision that needs a response - use a Dialog
Accessibility
Role: note
No keyboard shortcuts
Screen reader: Rendered as an <aside role=note> so assistive tech announces it as a supplementary region in natural DOM order. The optional title is a real lead-in read before the body. The leading icon is decorative (aria-hidden).
Notes: Do not rely on tone colour alone. Include plain text such as Note, Warning, or Error (often as the title) when the semantic state matters.
Props
Name
Type
Default
Description
tone
enum: note | info | success | warning | error
note
title
string
—
Optional bold lead-in title.
icon
string
—
Optional decorative leading icon key (e.g. info, warning).
Examples
Warning note
A documentation gotcha beside the prose it qualifies.
YAML
type: callout
props:
tone: warning
title: Heads up
icon: warning
slots:
default: Regenerating overwrites every file under generated/. Commit the YAML edit and the regenerated output together.
Info tip
An informational tip inside a guide.
YAML
type: callout
props:
tone: info
title: Tip
icon: info
slots:
default: Element behaviour flows from chemistry YAML through generated code — never hardcode element kinds in view code.
Plain note
A plain neutral aside with no title.
YAML
type: callout
props:
tone: note
slots:
default: This view is read-only for guest sessions.
Tones
All tones for visual comparison.
YAML
type: stack
props:
gap: sm
direction: column
children:
- type: callout
props:
tone: note
title: Note
slots:
default: Neutral aside.
- type: callout
props:
tone: info
title: Info
slots:
default: Informational tip.
- type: callout
props:
tone: success
title: Done
slots:
default: Positive guidance.
- type: callout
props:
tone: warning
title: Caution
slots:
default: Watch out for this.
- type: callout
props:
tone: error
title: Error
slots:
default: Do not do this.
A ready-made card-shaped loading placeholder — a title bar plus three text lines (tapered widths) inside a card frame. It is intentionally zero-config: the fixed shape is the point, so a grid of loading cards reads as one coherent set instead of a jitter of differently-shaped placeholders. Render one per expected card while a card list loads.
Readiness
complete
Preview
live
Props
1
Examples
2
Code
implementation mapped
When to use
Loading state for a card list/grid — render N CardSkeletons matching the expected count.
First-load of a dashboard of Card tiles, before data arrives.
Anywhere you'd otherwise hand-fill a `<Card>` with LoadingSkeleton blocks.
When not to use
List rows (avatar + lines) — use `<ListSkeleton>`.
Table rows — use `<TableRowSkeleton>`.
An inline 'loading…' affordance — use `<LoadingInline>`.
Accessibility
Role: status
No keyboard shortcuts
Screen reader: `role=status` so AT announces the loading state without stealing focus. The skeleton bars are decorative; pair the loading region with a single polite "Loading…" announcement at the container level rather than one per skeleton.
Notes: Zero-config by design — the fixed title+3-line shape keeps a loading grid visually stable. Do not add per-instance shape props; that would reintroduce the jitter this primitive exists to remove.
Props
Name
Type
Default
Description
shape
enum: fixed
fixed
Documentation-only. CardSkeleton takes no runtime props — its shape (title + 3 tapered text lines in a card frame) is fixed by design for grid consistency. Listed so the catalog records the contract; not settable.
Examples
Card-list loading grid
A loading grid — three CardSkeletons standing in for a card list.
An empty-container affordance combining icon + title + body + optional CTA. EmptyState exists so "this list is empty" never reads as a dead-end — there is always a next action available (create something, invite someone, change the filter). For panel-bound empty states use PanelEmptyState (sibling, panel-density-aware).
Readiness
complete
Preview
live
Props
3
Examples
2
Code
implementation mapped
When to use
Empty list / table / search-result panel
First-run state for a feature ('No agents yet')
Filtered view with zero matches ('No agents match your filters')
Permission-gated views ('Sign in to see your projects')
When not to use
Loading state — use LoadingSpinner / LoadingSkeleton
Error state — use Banner / PanelErrorBanner
Inside a panel where panel-density-aware variant is needed — use PanelEmptyState
Accessibility
Role: status
No keyboard shortcuts
Screen reader: Reads heading + body via natural DOM order. CTA is announced as a button with its label. Keep heading concise — SR users hear it first and parse the rest in order.
Notes: The icon is decorative — set aria-hidden=true so SR users don't hear it announced. The textual content carries the meaning.
Props
Name
Type
Default
Description
title
string
—
Short heading explaining the empty state.
body
string
—
Longer prose describing what to do next.
icon
string
—
Icon name (decorative — aria-hidden).
Examples
Empty agents
Empty agent list with create CTA.
No agents yet
Create an agent to start automating tasks for this circle.
YAML
type: empty-state
props:
title: No agents yet
body: Create an agent to start automating tasks for this circle.
icon: agents
slots:
cta:
- type: button
props:
variant: primary
on_click: ${actions.create_agent}
slots:
default: Create agent
No matches
Filtered list with no matches and a clear-filter CTA.
A list-shaped loading placeholder — `count` rows, each an avatar plus a title line (40%) and a text line (70%). It mirrors the shape of a loaded list so the surface does not reflow when real data arrives. Set `count` to the expected row count where known; the default of 3 is a calm generic.
Readiness
complete
Preview
live
Props
1
Examples
2
Code
implementation mapped
When to use
Loading state for an avatar+text list (members, sessions, recent activity).
First-load of a panel list, sized to the expected row count to avoid reflow.
Anywhere you'd hand-fill an `<ItemList>` with LoadingSkeleton rows.
When not to use
Card grids — use `<CardSkeleton>`.
Table rows — use `<TableRowSkeleton>`.
A single inline 'loading…' — use `<LoadingInline>`.
Accessibility
Role: status
No keyboard shortcuts
Screen reader: `role=status`; skeleton rows are decorative. Announce loading once at the container level (polite), not once per skeleton row, so AT users aren't flooded.
Notes: Match `count` to the expected result size when you can — a skeleton that reflows to a very different row count on load defeats the no-reflow purpose.
Props
Name
Type
Default
Description
count
integer
3
Number of skeleton rows. Set to the expected loaded row count to avoid reflow; default 3 is a generic calm placeholder.
A small spinner plus a short text label, laid out inline for in-flow loading — inside a button, beside a row action, mid-sentence. Use it for "something is happening right here, briefly" (saving, sending, reconnecting), not for first-load of a whole region (that is a skeleton's job). `text` defaults to "Loading…".
Readiness
complete
Preview
live
Props
1
Examples
2
Code
implementation mapped
When to use
Inline action feedback — "Saving…", "Sending…", "Reconnecting…" beside or inside a control.
A brief pending state where the surrounding layout should not shift.
Replacing a bare 'Loading...' string with a consistent spinner+text affordance.
When not to use
First-load of a list/card/table region — use the matching skeleton (preserves layout shape).
A full-panel loading state — use `<PanelLoadingState>`.
A long operation with progress — use a progress indicator, not an indefinite inline spinner.
Accessibility
Role: status
No keyboard shortcuts
Screen reader: `role=status` so the pending state is announced politely. Keep `text` a short verb phrase ("Saving…") — it is the announced content; the spinner is decorative. Clear/replace it promptly when the operation resolves so AT users get the outcome.
Notes: Indefinite by nature — for operations long enough to need ETA or percentage, use a determinate progress indicator instead.
Props
Name
Type
Default
Description
text
string
Loading...
Short verb-phrase label beside the spinner ("Saving…", "Sending…"). It is the SR-announced content; keep it terse.
Examples
Inline saving
In-button saving state — spinner + 'Saving…'.
Saving…
YAML
type: stack
props:
direction: row
gap: sm
children:
- type: spinner
- type: text
slots:
default: Saving…
Default loading
Default label beside a row action.
Loading…
YAML
type: stack
props:
direction: row
gap: sm
children:
- type: spinner
- type: text
slots:
default: Loading…
A content-shaped placeholder that shimmers while the real content is loading. Use Skeleton (not Spinner) when the layout would otherwise jump as content lands, or when the user benefits from knowing the shape of what's coming. For specific common shapes, use the dedicated variants: CardSkeleton, ListSkeleton, TableRowSkeleton.
Readiness
complete
Preview
live
Props
3
Examples
2
Code
implementation mapped
When to use
Above-the-fold content where layout shift is jarring
Repeating list / card layouts (use specific variants)
Long-running fetches where shape signals to the user what to expect
Pre-rendering placeholders for SSR / hydration windows
When not to use
Brief loads (<200ms) — show nothing
Action affordances (button click, form submit) — use Spinner
Errors — use Banner / EmptyState
Content with unknown shape — Spinner is better than a misleading skeleton
Accessibility
Role: status
No keyboard shortcuts
Screen reader: Skeleton elements should declare aria-busy=true on their containing region so SR users know content is loading. The shimmer animation itself is not announced.
Notes: Don't use Skeleton for content that may never load (e.g. permission- gated). Use EmptyState instead.
Props
Name
Type
Default
Description
width
string
—
CSS width (e.g. '100%', '120px').
height
string
—
CSS height (e.g. '1rem', '24px').
rounded
boolean
false
Whether to round corners (for avatar / pill placeholders).
The non-visual coordination layer for transient notifications: a single bus that `<Toast>`, ambient-notification (`unified_bar/ambient_renderer`), and optimistic-error-toast (`optimistic/components`) should all publish into and render from, instead of three parallel systems. It owns the notification model — kind (info/success/warning/error), tier, optional celebration/confirm payloads, and actions — while visual treatment lives in the renderers. It lives in `portal/src/canvas/notification_bus.rs` and is catalogued so future visual work routes through it.
Readiness
complete
Preview
live
Props
3
Examples
2
Code
implementation mapped
When to use
Emitting any transient notification — publish to the bus, let a renderer (Toast / ambient) display it.
Building a new notification surface — subscribe to the bus, do NOT invent a parallel queue.
Coordinating dedupe / tiering across notification sources.
When not to use
Rendering — this is coordination only; visual treatment is `<Toast>` / ambient renderers.
Persistent state or audit logs — the bus is for transient notifications, not durable records.
A one-off in-component message with no cross-system concern — a local signal is simpler.
Accessibility
Role: presentation
No keyboard shortcuts
Screen reader: Not rendered — no direct AT surface. Its accessibility value is indirect: by funnelling all transient notifications through one bus, the renderers can own exactly one well-behaved `aria-live` region instead of three competing ones talking over each other.
Notes: Coordination layer — visual + aria-live treatment lives in `<Toast>` / `<ToastStack>` / ambient renderers. Catalog records the shared contract for `portal::canvas::notification_bus::NotificationBus`.
Props
Name
Type
Default
Description
kind
enum: info | success | warning | error
—
Notification semantic kind. Renderers map this to tone/icon/ aria-live politeness. Required on publish.
tier
enum: ambient | toast | celebration | confirm
—
Which renderer/treatment the notification routes to — ambient bar, transient toast, celebration, or a confirm prompt.
actions
array
—
Optional action descriptors carried with the notification so the renderer can offer buttons (Retry, Undo, View).
Examples
Publish: success → toast
Publishing a success notification routed to the toast tier (data shape, not a render).
A linear progress indicator for a single quantifiable task: an upload, an import, a quota fill, a multi-file operation. In determinate mode the fill width tracks value/max and the rounded percentage is announced via aria-valuenow. In indeterminate mode the bar shows a travelling segment and drops aria-valuenow so assistive tech says "busy" instead of a misleading number. For multi-step flow position use ProgressSteps; for a tiny liveness marker use StatusDot.
Readiness
complete
Preview
live
Props
6
Examples
4
Code
implementation mapped
When to use
Upload / download / import progress with a known total
A quota or usage meter (value out of max)
A long operation whose completion is unknown (indeterminate mode)
Batch operation progress ('312 of 500 processed')
When not to use
Discrete steps in a wizard — use ProgressSteps
A tiny on/off liveness marker — use StatusDot
A radial / dial visual — use Gauge or DonutChart
An indeterminate spinner with no bar semantics — use LoadingSpinner
Accessibility
Role: progressbar
No keyboard shortcuts
Screen reader: The track is role=progressbar with aria-valuemin=0 and aria-valuemax=max. In determinate mode aria-valuenow carries the clamped current value; in indeterminate mode aria-valuenow is omitted so assistive tech announces a busy state rather than a false percentage. The visible label also backs the accessible name.
Notes: Always pass a label so the bar has an accessible name. The numeric percentage is rendered as text (show_value) so it is not colour- or width-only.
Props
Name
Type
Default
Description
value
number
0
Current value, clamped to 0..=max. Ignored when indeterminate.
max
number
100
Upper bound. Non-positive values fall back to 100.
A horizontal indicator of position within a discrete, ordered flow: an onboarding wizard, a multi-page form, a setup checklist. Steps before the current one render complete (check marker); the current step is emphasised; later steps are upcoming. State is conveyed by marker glyph plus colour plus visually-hidden text, never colour alone. For a continuous quantity use ProgressBar; for a single liveness marker use StatusDot.
Readiness
complete
Preview
live
Props
2
Examples
3
Code
implementation mapped
When to use
An onboarding or setup wizard ('Account → Profile → Confirm')
A multi-page form where the user should see how far they are
A checkout / submission flow with discrete stages
Any bounded sequence where step position matters
When not to use
Continuous / percentage progress — use ProgressBar
A single liveness marker — use StatusDot
Free navigation between unrelated sections — use Tabs
An unbounded activity feed — use a Timeline
Accessibility
Role: navigation
No keyboard shortcuts
Screen reader: Wrapped in <nav aria-label="Progress"> with an ordered list. The in-progress step carries aria-current="step". Each step appends visually-hidden text ('(complete)' / '(current)' / '(upcoming)') and a 'Step N of M' summary so the position is conveyed without relying on the marker colour. Markers are aria-hidden decoration.
Notes: State is conveyed by glyph (check vs number), colour, and screen-reader text together — never colour alone.
Props
Name
Type
Default
Description
steps
array
—
Ordered step labels.
current
integer
0
Zero-based index of the in-progress step. Earlier steps are complete; later steps are upcoming.
Examples
Onboarding
A three-step onboarding wizard on the middle step.
Visible spinner plus a polite live region for the status text. Always pair with a label slot — even decorative spinners benefit from a label hidden to sighted users but read by SR. Use `inline` when the spinner sits next to text in a row, `block` when it owns its own line.
Readiness
complete
Preview
live
Props
2
Examples
3
Code
implementation mapped
When to use
Known operation in progress (>250ms wait)
User-initiated action awaiting server response
Inline next to a button label (`layout: inline`)
Block-level loading screen for a section (`layout: block`)
Background refresh of visible data (with subtle inline placement)
When not to use
Skeleton-shaped placeholders for content layout — use LoadingSkeleton
Brief flashes (<100ms) — no indicator at all
Page navigation — use a route progress bar
Indeterminate operations >10s — switch to a progress bar with cancel
Accessibility
Role: status
No keyboard shortcuts
Screen reader: `role=status` + `aria-live=polite` ensures SR announces the label text when it changes (e.g. "Verifying molecular signature..." → "Almost there..."). Decorative spinners SHOULD still have a label hidden via visually-hidden CSS so SR users get an announcement.
Notes: The label is the load-bearing accessibility input. A spinner without a label is invisible to SR users entirely.
Props
Name
Type
Default
Description
size
enum: small | medium | large
medium
layout
enum: inline | block
block
`inline` keeps the spinner on the baseline of its row (label sits next to it). `block` centres the spinner above its label.
Examples
Block spinner with status
Login overlay loading state with announced status text.
YAML
type: spinner
props:
size: medium
layout: block
slots:
label:
- type: text
props:
kind: muted
slots:
default: ${state.loading_phase}
Inline spinner
Inline spinner in a button's loading state (handled by button itself, here for reference).
YAML
type: spinner
props:
size: small
layout: inline
Sizes gallery
All sizes side-by-side.
small
medium
large
YAML
type: stack
props:
gap: lg
direction: row
align: center
children:
- type: spinner
props:
size: small
layout: block
slots:
label:
- type: text
slots:
default: small
- type: spinner
props:
size: medium
layout: block
slots:
label:
- type: text
slots:
default: medium
- type: spinner
props:
size: large
layout: block
slots:
label:
- type: text
slots:
default: large
A small coloured dot communicating connection / liveness state at a glance: online, offline, error, idle, busy, or unknown. Colour is never the sole signal — the component always supplies an accessible name from the tone, and an optional visible label is encouraged. The pulse option draws attention to an active state and is suppressed under reduced motion. For a labelled status use Badge; for numeric progress use ProgressBar.
Readiness
complete
Preview
live
Props
3
Examples
4
Code
implementation mapped
When to use
Agent / service liveness next to a name ('● Online')
Connection state in a header or rail item
Row-level health indicator in a dense list
A pulsing 'live' marker on an actively streaming resource
When not to use
A textual status label or count — use Badge
Linear completion progress — use ProgressBar
Multi-step flow position — use ProgressSteps
An interactive toggle — use Switch
Accessibility
Role: status
No keyboard shortcuts
Screen reader: The wrapper is role=status with an aria-label drawn from the tone (e.g. 'Online') when no visible label is given, so the state is announced even though the dot itself is aria-hidden decoration. A supplied visible label backs the accessible name instead.
Notes: Pair with a text label — colour alone fails for colourblind users. The default per-tone label exists precisely so a dot-only usage is still announced.
One `<tr>` of `columns` placeholder-text cells. Render several inside a `<tbody>` while table data loads so column widths and row height stay stable — the table does not jump when real rows replace the skeletons. Match `columns` to the real table's column count.
Readiness
complete
Preview
live
Props
1
Examples
2
Code
implementation mapped
When to use
Loading state for a data table — N TableRowSkeletons in the tbody.
Keeping column widths stable across the load transition.
Anywhere you'd hand-fill `<tr>`s with LoadingSkeleton cells.
When not to use
Non-table lists — use `<ListSkeleton>`.
Card grids — use `<CardSkeleton>`.
A single inline 'loading…' — use `<LoadingInline>`.
Accessibility
Role: status
No keyboard shortcuts
Screen reader: `role=status`; cells are decorative. Announce loading once at the table/container level (polite) rather than per skeleton row. Ensure the real table has proper header associations once loaded.
Notes: Set `columns` to the real column count — a skeleton with the wrong column count reflows the table on load, defeating the purpose.
Props
Name
Type
Default
Description
columns
integer
4
Number of placeholder cells in the row. Match the real table's column count to keep widths stable across load.
Examples
Table loading (columns=5)
5-column table loading — three skeleton rows.
▬▬ ▬▬ ▬▬ ▬▬ ▬▬▬▬ ▬▬ ▬▬ ▬▬ ▬▬▬▬ ▬▬ ▬▬ ▬▬ ▬▬
YAML
type: stack
props:
direction: column
gap: sm
children:
- type: text
slots:
default: ▬▬ ▬▬ ▬▬ ▬▬ ▬▬
- type: text
slots:
default: ▬▬ ▬▬ ▬▬ ▬▬ ▬▬
- type: text
slots:
default: ▬▬ ▬▬ ▬▬ ▬▬ ▬▬
A floating, auto-dismissing transient notification. Call sites push via `ToastContext` — `push(msg, level)`, `push_icon_only(level)`, `push_with_action(msg, level, …)`, `dismiss(id)` — and the toast renders + auto-expires in the `<ToastStack>` mounted at app root. `level` is the shared tone enum (info / success / warning / error).
Readiness
complete
Preview
live
Props
4
Examples
2
Code
implementation mapped
When to use
Confirming a completed action that doesn't need acknowledgement — "Changes saved".
Non-blocking warnings/errors the user should notice but not be interrupted by.
Icon-only success pings for high-frequency low-importance actions (`push_icon_only`).
When not to use
Errors that must be acted on before continuing — use a `<PanelErrorBanner>` or modal.
Persistent status — use `<WorkspaceStatus>` / `<PanelFooter>` status slot.
A new parallel notification path — route through `<NotificationBus>`; don't add a 4th system.
Accessibility
Role: status
Escape — Dismiss the focused/last toast.
Screen reader: `aria-live=polite` for info/success/warning so the toast is announced without interrupting; `aria-live=assertive` for errors. Auto-dismiss timing must be long enough for SR users to hear it — pair critical content with `push_with_action` so it isn't lost to a timeout.
Notes: `portal::components::toast::Toast` provides the presentational row; `ToastContext` remains the app-facing push API.
Props
Name
Type
Default
Description
message
string
—
Toast body text. Required (except `push_icon_only`).
level
enum: info | success | warning | error
info
Shared tone enum (`ToastLevel`). Drives colour + aria-live politeness.
icon_only
boolean
false
Render just the level icon (high-frequency low-importance pings).
action
string
—
Optional action label (`push_with_action`) — renders a button so the toast survives long enough to be acted on.
Examples
Success toast
Success confirmation — the canonical 'Changes saved' ping.
The singleton container that makes `<Toast>` work: mounted once at the app root, it subscribes to the toast bus, renders queued toasts with stacking rules and exit transitions, and owns the `aria-live` region toasts announce into. Call sites never render it directly — they `push` via `ToastContext` and ToastStack does the rest.
Readiness
complete
Preview
live
Props
2
Examples
2
Code
implementation mapped
When to use
Exactly once, at the application root, so every surface's toasts have a renderer.
As the single mount point — never per-panel; multiple stacks fight over the same queue.
When wiring a new app shell that needs toast support.
When not to use
Per-surface or per-panel — the stack is a singleton by contract.
To push a toast — use `ToastContext::push`, not a direct render.
As a general overlay container — it is toast-queue-specific.
Accessibility
Role: region
No keyboard shortcuts
Screen reader: Owns the `aria-live` region; individual toasts set polite/assertive per level. As a singleton it ensures there is exactly one live region for transient notifications — multiple stacks would create competing live regions that talk over each other.
Notes: `portal::components::toast::ToastStack` is the singleton mount point for ambient toast rendering.