# Alert

> A message block with role="status" or role="alert" and a color: information, success, warning or error.

Page: https://svelte-lean.vercel.app/primitives/alert · Markdown: https://svelte-lean.vercel.app/primitives/alert.md · Index: https://svelte-lean.vercel.app/llms.txt

A message block with a color (information, success, warning or error) and a variant. Use it for messages that belong to the page or follow an action.

```svelte
<script lang="ts">
	import '@svelte-lean/styles/alert.css';
</script>

<div data-slean="alert" data-color="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>
```

A message rendered with the page carries no live role.

## Examples

### Colors

```svelte
<script lang="ts">
	import '@svelte-lean/styles/alert.css';
</script>

<div data-slean="alert" data-color="info">
	<span data-slean-part="icon" aria-hidden="true"></span>
	<p data-slean-part="title">A new version is available</p>
</div>
<div data-slean="alert" data-color="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-color="warning">
	<span data-slean-part="icon" aria-hidden="true"></span>
	<p data-slean-part="title">Two seats left on this plan</p>
</div>
<div data-slean="alert" data-color="error">
	<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>
<div data-slean="alert">
	<p data-slean-part="title">No color: a neutral note</p>
</div>
```

Each status color has its own mark, so the meaning does not rest on color.

### Looks

```svelte
<script lang="ts">
	import '@svelte-lean/styles/alert.css';

	const variants = ['standard', 'filled', 'outlined'];
	const severities = ['success', 'info', 'warning', 'error'];
</script>

{#each variants as variant}
	{#each severities as color}
		<div data-slean="alert" data-variant={variant} data-color={color}>
			<span data-slean-part="icon" aria-hidden="true"></span>
			<p data-slean-part="title">A {variant} {color} alert</p>
		</div>
	{/each}
{/each}
```

standard is the soft tint (the default), filled the main color, outlined a line in the color without a fill.

### Status after an action

```svelte
<script lang="ts">
	import '@svelte-lean/styles/alert.css';
	import '@svelte-lean/styles/button.css';

	let saved = $state(false);
</script>

<button type="button" data-slean="button" data-variant="outlined" data-color="neutral" onclick={() => (saved = true)}>
	Save changes
</button>

<!-- In the page from the start and empty, so the change is announced. -->
<div data-slean="alert" data-color="success" role="status">
	{#if saved}
		<span data-slean-part="icon" aria-hidden="true"></span>
		<p data-slean-part="title">Changes saved</p>
	{/if}
</div>
```

The empty region is in the page from the start, so writing into it is announced.

### With actions

```svelte
<script lang="ts">
	import '@svelte-lean/styles/alert.css';
	import '@svelte-lean/styles/button.css';
</script>

<div data-slean="alert" data-color="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="small">Choose a plan</button>
		<button type="button" data-slean="button" data-size="small" data-variant="text" data-color="neutral">Later</button>
	</div>
</div>
```

### Dismissible

```svelte
<script lang="ts">
	import '@svelte-lean/styles/alert.css';

	let open = $state(true);
</script>

<!-- The close part is styled; removing the alert is the application's. -->
{#if open}
	<div data-slean="alert" data-color="info">
		<span data-slean-part="icon" aria-hidden="true"></span>
		<p data-slean-part="title">A new version is available</p>
		<p data-slean-part="description">Reload the page to use it.</p>
		<button type="button" data-slean-part="close" aria-label="Dismiss" onclick={() => (open = false)}></button>
	</div>
{/if}
```

An empty close part draws the close icon; name it in the page's language. The application removes the alert.

### Custom icon

```svelte
<script lang="ts">
	import '@svelte-lean/styles/alert.css';
</script>

<!-- An icon part with content is only sized: bring your own SVG. -->
<div data-slean="alert" data-color="info">
	<span data-slean-part="icon" aria-hidden="true">
		<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
			<rect x="3" y="4" width="18" height="18" rx="2" />
			<path d="M16 2v4M8 2v4M3 10h18" />
		</svg>
	</span>
	<p data-slean-part="title">Maintenance on Sunday</p>
	<p data-slean-part="description">The dashboard is read-only from 02:00 to 04:00 UTC.</p>
</div>
```

An icon part with content is sized and tinted, not drawn over.

### Urgent message

```svelte
<script lang="ts">
	import '@svelte-lean/styles/alert.css';
	import '@svelte-lean/styles/button.css';

	let expiring = $state(false);
</script>

<button type="button" data-slean="button" data-variant="outlined" data-color="neutral" onclick={() => (expiring = true)}>
	Simulate expiry
</button>

<!-- Inserted with role="alert": announced at once, focus stays where it is. -->
{#if expiring}
	<div data-slean="alert" data-color="error" 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="small" onclick={() => (expiring = false)}>
				Stay signed in
			</button>
		</div>
	</div>
{/if}
```

role="alert" interrupts the screen reader; keep it for messages that need attention now.

## Usage

Plain markup with a stylesheet: install `@svelte-lean/styles`. No JavaScript runs.

```bash
npm install @svelte-lean/styles
```

`stylesheets`

```ts
import '@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 color with `data-color` (`info`, `success`, `warning`, `error`; neutral by default) and the fill with `data-variant` (`standard`, `filled`, `outlined`). 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-color="info\|success\|warning\|error" (neutral by default), data-variant="standard\|filled\|outlined". 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 color. With an SVG inside: sized. |
| title | `<p> or a heading` | `title` | no | Semibold, in the text color of the variant. |
| description | `<p> or <div>` | `description` | no | The body, in the text color. |
| actions | `<div>` | `actions` | no | Buttons or links in a wrapping row. |
| close | `<button type="button" aria-label="…">` | `close` | no | At the end of the first line, in a column of its own. Empty: the close icon. The application removes the alert on click. |

## Keyboard

| Key | When | Result |
| --- | --- | --- |
| `Tab`/`Shift+Tab` | links or buttons inside | Moves focus to them (native); the alert itself is not focusable |

## Styling

`alert.css` lays the parts out in two columns (the icon, then the text), tints the surface and the border from the current color (`data-color`) per variant, and colors the title and the icon with the variant's text color. An empty icon part draws an i, a check, an exclamation mark or a cross; 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` | `calc(var(--slean-spacing) * 1)` | row-gap |
| `--slean-space-3` | `calc(var(--slean-spacing) * 3)` | padding-block, margin-inline-end |
| `--slean-space-4` | `calc(var(--slean-spacing) * 4)` | padding-inline |
| `--slean-color-main` | `var(--slean-primary-main)` | border, background, border-color |
| `--slean-radius-md` | `var(--slean-radius)` | border-radius |
| `--slean-color-soft` | `var(--slean-primary-soft)` | background |
| `--slean-fg` | `var(--slean-neutral-12)` | color |
| `--slean-text-sm` | `0.875rem` | font-size |
| `--slean-leading` | `1.5` | line-height, margin-block |
| `--slean-font-weight-semibold` | `600` | font-weight |
| `--slean-space-2` | `calc(var(--slean-spacing) * 2)` | gap, margin-block-start, margin-inline |
| `--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-control-height-sm` | `2rem` | inline-size, block-size, margin-block |
| `--slean-radius-sm` | `calc(var(--slean-radius) * 0.6)` | border-radius |
| `--slean-icon-close` | `url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3E%3Cpath d='M4.5 4.5l7 7m0-7l-7 7' stroke='black' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round' fill='none'/%3E%3C/svg%3E")` | mask |
| `--slean-color-soft-text` | `var(--slean-primary-soft-text)` | color |
| `--slean-color-contrast-text` | `var(--slean-primary-contrast-text)` | color |

State selectors the stylesheet targets, all from the platform or ARIA: `:active`, `:empty`, `:focus-visible`, `:hover`.

Variant attributes: `data-color` (`success`, `warning`, `error`); `data-variant` (`filled`, `outlined`).

## Under the hood

Runtime and size

- **Tier**: 0 · Native
- **Behavior JS**: 0 B brotli · 0 B gzip · `fixtures/results.json (native-only)` · [method](https://svelte-lean.vercel.app/performance/methodology.md)
- **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)`

Tier 0: no behavior module; the Vite plugin maps `alert` to no module. The live region is the browser’s; the status and urgent examples use application state, not a package runtime.

Why this implementation

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 standard, filled and outlined variants from the current color and one mark per status color, so the meaning 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: standard, filled and outlined variants in every color, 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

Accessibility

- 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 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.

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 outlined 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 and hydration

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.

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.

Compatibility

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 colored borders is Baseline 2023; without it the border is the neutral one.

Tests and source

- `apps/playground/tests/primitives/feedback-navigation.spec.ts` Playwright: roles, names and states in the accessibility tree, keyboard, RTL, reduced motion, axe
- `packages/styles/tests` static checks of the stylesheet (layers, tokens, specificity, dark parity)

- `packages/primitives/src/alert/contract.md` the contract in prose: semantics, keyboard, focus, ARIA, SSR, JS-disabled behavior
- `packages/primitives/src/alert/contract.ts` typed constants: name, tier, base, parts, options, events
- `packages/styles/css/alert.css` the optional stylesheet
