Components / feedback
Toast
Radix Toast was not used. The six patterns Radix is permitted for are a closed list, and a toast needs no focus scope, no collision detection and no portal.
Summary
A confirmation that something happened, shown briefly and then gone.
| APG pattern | none |
| Built on | native elements |
| Status | stable |
| Since | 0.2.0 |
The visual is trivial. The timing is the whole specification.
<Toast.Region>
{toasts.map((t) => (
<Toast key={t.id} intent="positive" onDismiss={() => remove(t.id)}>Portfolio published</Toast>
))}
</Toast.Region>
Anatomy
Two elements. Toast.Region is the live region, fixed to a corner and always present. Each Toast is a row inside it.
| Part | Element | Required |
|---|---|---|
action | <div> | no |
close | <button> | no |
content | <div> | yes |
icon | <span> | no |
title | <span> | no |
The region is pointer-events: none so an empty one does not swallow clicks on the page beneath; each toast takes them back.
Properties
| Property | Values | Default | What it does |
|---|---|---|---|
intent | neutral · positive · warning | neutral | The tint of the leading edge. There is no negative: a failure belongs inline as an Alert, because a message that matters must not be on a timer. |
duration | number | null | 6000 | Milliseconds before onDismiss fires. null never dismisses on its own, which is what a toast carrying an action must use — an action the reader cannot reach in time is worse than no action. The timer pauses while the pointer is over the toast or focus is inside it. |
onDismiss | () => void | none | Without this the toast has no timer and no close button: nothing can remove it. |
dismissLabel | string | Dismiss | The close button's accessible name. Its visible content is a glyph, which announces as nothing useful. |
icon | React.ReactNode | none | An <Icon>, in the same slot Alert puts one. |
title | string | none | A short heading above the message, the same slot and the same treatment as Alert's. Omit it when the message is one sentence — two lines saying the same thing is one too many, and a toast is on a timer. |
action | React.ReactNode | none | One control. More than one means this should have been a Dialog. |
children | React.ReactNode | none | The message. One sentence, past tense — see the spec's Content section. |
States
| State | Expressed as | |
|---|---|---|
| Focused | :focus-visible | |
| Hover | :hover |
data-paused is set while the pointer is over the toast or focus is inside it.
Behavior
The timer pauses on hover and on focus, and resumes with the time already served rather than a fresh duration. A reader whose pointer is on the toast, or who has tabbed to its dismiss button, is reading it — taking it away mid-sentence is the defect this prevents, and WCAG 2.2.1 asks for time limits to be adjustable.
duration={null} never dismisses. With no onDismiss, there is no timer and no close button at all.
Accessibility
Polite only, and that is a decision. One live region, aria-live="polite". Anything urgent, anything the reader must act on, and anything reporting a failure belongs inline as an Alert. A message that matters must not be on a timer, and a message on a timer must not matter.
The region renders whether or not it holds anything. A live region created in the same moment as its content announces nothing in most screen readers. This is the single most common way a toast is built and silently never heard.
A keyboard reader reaches a toast last, because it sits at the end of the document. That is why a toast carrying an action must not auto-dismiss.
Keyboard
None of its own. Tab reaches the dismiss button and any action, and focus inside the toast pauses its timer.
Roles, states and properties
| Attribute | Set by |
|---|---|
aria-hidden | always true |
aria-label | set at render |
aria-live | always polite |
aria-relevant | always additions |
role | always region |
Focus
A keyboard reader reaches a toast last, because the region sits at the end of the document. Nothing moves focus into it — a notification that stole focus would interrupt exactly what it is reporting on. There is no Escape handler: the toast is dismissed by its own control or by its timer.
Screen reader
Not recorded as verified. Stage 4 of the readiness ladder — driving each component with VoiceOver and NVDA — is unstarted across the package, and no component should claim otherwise until docs/journal/ records the session that did it.
Contrast and target size
npm run check:contrast audits every color pair this component uses across all four skins, light and dark, and fails the build on a shortfall. Two waivers are on the record in docs/audit/DEFERRED.md.
It sets no target size of its own; anything interactive inside it brings its own.
Tested
test/browser/feedback.spec.mjs
Known gaps
_No upstream requirement is recorded as not applicable._
Appearance
| Token | Used for |
|---|---|
--se-border | border |
--se-border-strong | border-inline-start |
--se-border-width | border |
--se-col-min | inline-size |
--se-duration-medium | animation |
--se-ease-decelerate | animation |
--se-focus-color | outline |
--se-focus-offset | outline-offset |
--se-focus-width | outline |
--se-icon-md | block-size, font-size, inline-size |
--se-icon-sm | block-size, font-size, inline-size |
--se-layer-overlay | z-index |
--se-leading-body | line-height |
--se-leading-ui | line-height |
--se-page-gutter | inline-size, inset-block-end, inset-inline-end |
--se-positive | intent |
--se-radius-md | border-radius |
--se-radius-sm | border-radius |
--se-shadow-floating | box-shadow |
--se-space-1 | border-inline-start, gap |
--se-space-2 | gap |
--se-space-3 | gap, padding |
--se-space-4 | padding |
--se-surface | background |
--se-surface-sunk | background |
--se-text | color |
--se-text-md | font-size |
--se-text-muted | color |
--se-text-sm | font-size |
--se-warning | intent |
--se-weight-medium | font-weight |
--se-shadow-floating is the level: it floats over the page and dismisses itself, which is what 3.5 reads a level as meaning.
Motion
Entry fades and rises over --se-duration-medium on --se-ease-decelerate. Leaving is not animated — the toast is removed by the caller, and an exit animation would need the caller to hold it in the tree past the point it decided it was done.
Content
One sentence, past tense, naming what happened. Portfolio published. Not Success! and not Your portfolio has been successfully published.
The verb carries through from the control that started it: Publish produces Published.
Usage
One action, at most. Two means the reader has a decision to make, and a decision on a timer belongs in a Dialog.
Where this stops: the queue is yours. This component shows what it is given and removes one item at a time. How many are visible at once, what happens to the eleventh, and whether a repeated message stacks or replaces are product decisions, and no default would survive contact with a second product. Three visible is a reasonable place to start.
When not to use it. If the reader will need the message again — an error to fix, an id to copy, a link to follow — it must not vanish. Put it in the page.
Notes
A second, assertive region was rejected. It turns every new message into an argument about which tier it earns, and the answer is almost always "not this one". The polite-only rule is what keeps this component small.
Radix Toast was not used. The six patterns Radix is permitted for are a closed list, and a toast needs no focus scope, no collision detection and no portal.