Primitives Feedback
Alert
A message block with a tone: information, success, warning or danger. The tone is a stylesheet; whether a screen reader announces the message depends on a live role and on when the message appears, and the contract says which role fits. Tier 0.
- Tier
- 0 · Native
- Behavior JS
- 0 B brotli · 0 B gzip ·
fixtures/· methodresults.json (native-only) - Platform features
role="status",role="alert",:empty,color-mix()- Shared listeners
- none
- Per-instance listeners
- none
- Lazy state
- none
- Native base
<div> + live region roles (status, alert)
On this page
Example
Exports include the time zone
CSV files now carry an offset column. Older imports keep working.
Domain verified
Mail for harbor.studio now arrives in the workspace.
The trial ends in three days
Choose a plan to keep the projects and their history.
The last deploy failed
The build stopped at the type check. The previous version is still live.
<div data-slean="alert" data-variant="info">
<span data-slean-part="icon" aria-hidden="true"></span>
<p data-slean-part="title">Exports include the time zone</p>
<p data-slean-part="description">CSV files now carry an offset column. Older imports keep working.</p>
</div>
<div data-slean="alert" data-variant="success">
<span data-slean-part="icon" aria-hidden="true"></span>
<p data-slean-part="title">Domain verified</p>
<p data-slean-part="description">Mail for harbor.studio now arrives in the workspace.</p>
</div>
<div data-slean="alert" data-variant="warning">
<span data-slean-part="icon" aria-hidden="true"></span>
<p data-slean-part="title">The trial ends in three days</p>
<p data-slean-part="description">Choose a plan to keep the projects and their history.</p>
<div data-slean-part="actions">
<button type="button" data-slean="button" data-size="sm">Choose a plan</button>
<button type="button" data-slean="button" data-size="sm" data-variant="ghost">Later</button>
</div>
</div>
<div data-slean="alert" data-variant="danger">
<span data-slean-part="icon" aria-hidden="true"></span>
<p data-slean-part="title">The last deploy failed</p>
<p data-slean-part="description">The build stopped at the type check. The previous version is still live.</p>
</div><script lang="ts">
// Nothing to import at runtime: the alert is markup. The stylesheets are optional.
import '@svelte-lean/styles/button.css';
import '@svelte-lean/styles/alert.css';
</script>
<div data-slean="alert" data-variant="info">
<span data-slean-part="icon" aria-hidden="true"></span>
<p data-slean-part="title">Exports include the time zone</p>
<p data-slean-part="description">CSV files now carry an offset column. Older imports keep working.</p>
</div>
<div data-slean="alert" data-variant="success">
<span data-slean-part="icon" aria-hidden="true"></span>
<p data-slean-part="title">Domain verified</p>
<p data-slean-part="description">Mail for harbor.studio now arrives in the workspace.</p>
</div>
<div data-slean="alert" data-variant="warning">
<span data-slean-part="icon" aria-hidden="true"></span>
<p data-slean-part="title">The trial ends in three days</p>
<p data-slean-part="description">Choose a plan to keep the projects and their history.</p>
<div data-slean-part="actions">
<button type="button" data-slean="button" data-size="sm">Choose a plan</button>
<button type="button" data-slean="button" data-size="sm" data-variant="ghost">Later</button>
</div>
</div>
<div data-slean="alert" data-variant="danger">
<span data-slean-part="icon" aria-hidden="true"></span>
<p data-slean-part="title">The last deploy failed</p>
<p data-slean-part="description">The build stopped at the type check. The previous version is still live.</p>
</div>Why this implementation exists
An alert is text in a tinted box. What decides whether a screen reader user hears it is the live region: role="status" for a polite message, role="alert" for an urgent one, and the rule that a region is announced when its content changes, not when it arrives together with its text.
Svelte Lean ships the box and the rule. alert.css draws the tones from the soft status tokens and one mark per tone, so the tone does not rest on color alone. An empty alert draws nothing, so the root can wait in the page as the status region the application writes into.
The browser owns
- the live region roles and what they imply (polite or assertive, atomic)
- handing the message to assistive technology when a live region changes
- the links and buttons inside the message
Svelte Lean owns
- alert.css: the neutral and four tone surfaces, the layout of the parts, the drawn icons
- an empty root that draws nothing, so it can wait in the page as a status region
- the contract: when a message carries a live role and which one
- documentation
Usage
The markup needs no package. Install @svelte-lean/styles for the stylesheet; @svelte-lean/primitives adds the typed contract and nothing at runtime.
npm install @svelte-lean/stylespnpm add @svelte-lean/stylesyarn add @svelte-lean/stylesbun add @svelte-lean/stylesimport '@svelte-lean/styles/alert.css';Choose the role by when the message appears. Rendered with the page: no role. Written after an action and not urgent: role="status" on an element that is already in the page. Urgent: role="alert".
Set the tone with data-variant. Put the meaning in the title: the color and the icon are not read.
Anatomy
| Part | Element | data-slean-part | Required | Notes |
|---|---|---|---|---|
| root | <div data-slean="alert"> | – | yes | data-variant="info|success|warning|danger". No role for a message present at load; role="status" or role="alert" for one written later. |
| icon | <span aria-hidden="true"> | icon | no | Empty: the stylesheet draws an i, a check, an exclamation mark or a cross by tone. With an SVG inside: sized. |
| title | <p> or a heading | title | no | Semibold, in the tone’s color. |
| description | <p> or <div> | description | no | The body, in the text color. |
| actions | <div> | actions | no | Buttons or links in a wrapping row. |
Runtime profile
Tier 0: the alert has no behavior module, the Vite plugin maps alert to no module, and the page ships no Svelte Lean JavaScript for it. The live region is the browser’s; the status example below uses one line of application state, not a package runtime.
Accessibility contract
- A message present at load has no live role: it is read in order with the rest of the page.
role="status"(polite) for a message written after an action, on a region that exists before the message: an empty alert root, or a wrapper that stays.role="alert"(assertive) only for a message that needs attention now.- The drawn icon is
aria-hidden="true"and has no text. - Showing an alert never moves focus; a message that needs an answer is an alert dialog.
Keyboard
| Key | When | Result |
|---|---|---|
| Tab/Shift+Tab | links or buttons inside | Moves focus to them (native); the alert itself is not focusable |
Platform features
| Feature | Baseline | Outside the target |
|---|---|---|
role="status", role="alert" | ARIA 1.1 live region roles, exposed by every current browser | Not applicable; speech timing differs between screen readers |
color-mix() for the tone border | Baseline 2023 | The border keeps the neutral border token |
Without JavaScript
A message rendered with the page is fully visible and readable. A message that appears after an action needs the application’s script, or a new page from the server on which the message is present at load.
Server rendering
Render standing messages on the server without a live role. Render the region for later messages empty, so it exists before the first message is written into it.
Before hydration
Nothing is attached. An empty status region rendered on the server is the same element after hydration, so the first message written into it is a change the screen reader announces.
Styling
alert.css lays the parts out in two columns (the icon, then the text), tints the surface and the border per tone from the soft status tokens, colors the title and the icon with the tone’s foreground and keeps the description in the text color. An empty icon part draws an i, a check, an exclamation mark or a cross from its two pseudo-elements; under forced colors the box keeps its border and the marks their shapes. The tokens it reads:
| Token | Default (light) | Applies to |
|---|---|---|
--slean-space-1 | 0.25rem | row-gap |
--slean-space-3 | 0.75rem | padding-block, margin-inline-end |
--slean-space-4 | 1rem | padding-inline |
--slean-border | var(--slean-neutral-6) | border |
--slean-radius-md | 0.625rem | border-radius |
--slean-muted | var(--slean-neutral-3) | background |
--slean-fg | var(--slean-neutral-12) | color |
--slean-text-sm | 0.875rem | font-size |
--slean-leading | 1.5 | line-height |
--slean-font-weight-semibold | 600 | font-weight |
--slean-space-2 | 0.5rem | gap, margin-block-start |
--slean-font-weight-medium | 500 | font-weight |
--slean-icon-info | url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cg stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'%3E%3Ccircle cx='8' cy='8' r='6.25'/%3E%3Cpath d='M8 7.5v3.5M8 5v.01'/%3E%3C/g%3E%3C/svg%3E") | mask |
--slean-icon-success | url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cg stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'%3E%3Ccircle cx='8' cy='8' r='6.25'/%3E%3Cpath d='M5.5 8.25l1.75 1.75 3.25-3.5'/%3E%3C/g%3E%3C/svg%3E") | mask-image |
--slean-icon-warning | url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cg stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'%3E%3Cpath d='M6.7 2.75a1.5 1.5 0 012.6 0l5.1 8.9a1.5 1.5 0 01-1.3 2.25H2.9a1.5 1.5 0 01-1.3-2.25z'/%3E%3Cpath d='M8 6.25v3M8 11.5v.01'/%3E%3C/g%3E%3C/svg%3E") | mask-image |
--slean-icon-error | url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cg stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'%3E%3Ccircle cx='8' cy='8' r='6.25'/%3E%3Cpath d='M10 6l-4 4M6 6l4 4'/%3E%3C/g%3E%3C/svg%3E") | mask-image |
--slean-info | var(--slean-accent) | border-color |
--slean-info-soft | var(--slean-accent-soft) | background |
--slean-info-soft-fg | var(--slean-accent-soft-fg) | color |
--slean-success | oklch(55% 0.15 150) | border-color |
--slean-success-soft | oklch(95.5% 0.04 150) | background |
--slean-success-soft-fg | oklch(40% 0.12 150) | color |
--slean-warning | oklch(76% 0.16 80) | border-color |
--slean-warning-soft | oklch(96% 0.05 85) | background |
--slean-warning-soft-fg | oklch(45% 0.12 75) | color |
--slean-danger | oklch(55% 0.2 25) | border-color |
--slean-danger-soft | oklch(95.5% 0.03 25) | background |
--slean-danger-soft-fg | oklch(45% 0.18 25) | color |
State selectors the stylesheet targets, all from the platform or ARIA: :empty.
Variant attributes: data-variant (success, warning, danger, info).
Compatibility notes
The live region roles are ARIA 1.1 and exposed by every current browser. Screen readers differ in when and how they speak a change, which is why the contract keeps to the patterns that hold across them. color-mix() for the tone borders is Baseline 2023; without it the border is the neutral one.
Examples
Status after an action
The region is in the page from the start and empty; an empty alert draws nothing. Saving writes the title into it, the box appears, and a screen reader announces the change. Focus stays on the button.
<script lang="ts">
let saved = $state(false);
</script>
<button type="button" data-slean="button" onclick={() => (saved = true)}>Save changes</button>
<!-- In the page from the start and empty, so the change is announced. -->
<div data-slean="alert" data-variant="success" role="status">
{#if saved}
<span data-slean-part="icon" aria-hidden="true"></span>
<p data-slean-part="title">Changes saved</p>
{/if}
</div>Urgent message
role="alert" is assertive: it interrupts what the screen reader is saying. It fits a
message that needs attention now, and an element inserted with it is announced.
<!-- Inserted when the session is about to expire: announced at once, focus stays. -->
<div data-slean="alert" data-variant="danger" role="alert">
<span data-slean-part="icon" aria-hidden="true"></span>
<p data-slean-part="title">The session ends in two minutes</p>
<div data-slean-part="actions">
<button type="button" data-slean="button" data-size="sm">Stay signed in</button>
</div>
</div>Testing
apps/playground/tests/primitives/feedback-navigation.spec.tsPlaywright: roles, names and states in the accessibility tree, keyboard, RTL, reduced motion, axepackages/styles/testsstatic checks of the stylesheet (layers, tokens, specificity, dark parity)
Source
packages/primitives/src/alert/contract.mdthe contract in prose: semantics, keyboard, focus, ARIA, SSR, JS-disabled behaviorpackages/primitives/src/alert/contract.tstyped constants: name, tier, base, parts, options, eventspackages/styles/css/alert.cssthe optional stylesheet