sveltelean Primitives
Versionv0.2.0 GitHub

Example

Standing messages

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>
Tier 0: the two sources differ only by the stylesheet imports. These messages are part of the page when it loads, so they carry no live role. Each icon is an empty element the stylesheet draws on. Everything here works with page JavaScript disabled.

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/styles
stylesheets
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 tone with data-variant. Put the meaning in the title: the color and the icon are not read.

Anatomy

PartElementdata-slean-partRequiredNotes
root<div data-slean="alert">–yesdata-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">iconnoEmpty: the stylesheet draws an i, a check, an exclamation mark or a cross by tone. With an SVG inside: sized.
title<p> or a headingtitlenoSemibold, in the tone’s color.
description<p> or <div>descriptionnoThe body, in the text color.
actions<div>actionsnoButtons 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

KeyWhenResult
Tab/Shift+Tablinks or buttons insideMoves focus to them (native); the alert itself is not focusable

Platform features

FeatureBaselineOutside the target
role="status", role="alert"ARIA 1.1 live region roles, exposed by every current browserNot applicable; speech timing differs between screen readers
color-mix() for the tone borderBaseline 2023The 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:

TokenDefault (light)Applies to
--slean-space-10.25remrow-gap
--slean-space-30.75rempadding-block, margin-inline-end
--slean-space-41rempadding-inline
--slean-bordervar(--slean-neutral-6)border
--slean-radius-md0.625remborder-radius
--slean-mutedvar(--slean-neutral-3)background
--slean-fgvar(--slean-neutral-12)color
--slean-text-sm0.875remfont-size
--slean-leading1.5line-height
--slean-font-weight-semibold600font-weight
--slean-space-20.5remgap, margin-block-start
--slean-font-weight-medium500font-weight
--slean-icon-infourl("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-successurl("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-warningurl("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-errorurl("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-infovar(--slean-accent)border-color
--slean-info-softvar(--slean-accent-soft)background
--slean-info-soft-fgvar(--slean-accent-soft-fg)color
--slean-successoklch(55% 0.15 150)border-color
--slean-success-softoklch(95.5% 0.04 150)background
--slean-success-soft-fgoklch(40% 0.12 150)color
--slean-warningoklch(76% 0.16 80)border-color
--slean-warning-softoklch(96% 0.05 85)background
--slean-warning-soft-fgoklch(45% 0.12 75)color
--slean-dangeroklch(55% 0.2 25)border-color
--slean-danger-softoklch(95.5% 0.03 25)background
--slean-danger-soft-fgoklch(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.

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

urgent message
<!-- 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

Source