Toast & htmx
The optional adapter layer. Nothing in the core stylesheet mentions htmx - components style in-flight state off aria-busy, which any framework can set - so this ships as a second bundle that maps htmx's classes onto those same visuals. A SvelteKit app loads the core and never this.
Reference
sk-toast-region
Render it once in the layout, empty. It must exist from first paint: a live region created at the same moment as its first message is frequently not announced at all, which is the single most common reason toast notifications are silent for screen reader users.
<div class="sk-toast-region" id="sk-toasts"
role="status" aria-live="polite" aria-atomic="false"></div>| Attribute | Type | Default | Notes |
|---|---|---|---|
data-position | "top" | "top-start" | "bottom-start" | bottom end |
sk-toast
An sk-alert with elevation. It arrives by out-of-band swap, so no HX-Trigger listener and no JavaScript of ours is involved: the server renders the markup and htmx puts it in the region.
<div class="sk-toast-region" id="sk-toasts" hx-swap-oob="beforeend">
<div class="sk-alert sk-toast" data-status="success" data-autohide="true">
...
</div>
</div>| Attribute | Type | Default | Notes |
|---|---|---|---|
data-autohide | "true" | - | Fades out after --sk-toast-duration. Pure CSS, so it hides rather than removes - swap with innerHTML instead of beforeend if you would rather not accumulate nodes. Disabled under prefers-reduced-motion. |
--sk-toast-duration | time | 5s | How long before auto-hide begins. |
hx-swap-oob | "beforeend" | "innerHTML" | - | beforeend stacks messages; innerHTML replaces. Pair innerHTML with autohide. |
sk-indicator
Hidden until a request is in flight. Requires sk-htmx.css. htmx sets .htmx-request on the requesting element or on whatever hx-indicator points at, so both selector forms are covered.
<button class="sk-button" hx-post="/save">
Save
<span class="sk-indicator"><span class="sk-spinner"></span></span>
</button>sk-swap, sk-busy
Opt-in transitions, not applied to every swap target: a blanket transition makes a fast local response feel slower than it is. Requires htmx's swap delay to be at least as long as the transition - hx-swap="innerHTML swap:150ms".
<div class="sk-swap" hx-get="/rows" hx-swap="innerHTML swap:150ms">...</div>
<section class="sk-busy" hx-get="/report">...</section>Examples
Toasts
Rendered inline here rather than in the fixed region, so they can be seen side by side. In an application they arrive in the corner region by out-of-band swap.
Settings saved
Your changes are live.Upload failed
The file was larger than 50 MB.Show markup
<div class="sink-toast-demo">
<div class="sk-alert sk-toast" data-status="success">
<span class="sk-alert-icon">
<svg class="sk-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<circle cx="12" cy="12" r="9">
</circle>
<path d="m8.5 12.5 2.5 2.5 4.5-5">
</path>
</svg>
</span>
<div class="sk-alert-content">
<p class="sk-alert-title">
Settings saved
</p>
Your changes are live.
</div>
</div>
<div class="sk-alert sk-toast" data-status="danger">
<span class="sk-alert-icon">
<svg class="sk-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<circle cx="12" cy="12" r="9">
</circle>
<path d="m15 9-6 6">
</path>
<path d="m9 9 6 6">
</path>
</svg>
</span>
<div class="sk-alert-content">
<p class="sk-alert-title">
Upload failed
</p>
The file was larger than 50 MB.
</div>
<button type="button" class="sk-alert-dismiss" aria-label="Dismiss">
<svg class="sk-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<path d="M18 6 6 18">
</path>
<path d="m6 6 12 12">
</path>
</svg>
</button>
</div>
<div class="sk-alert sk-toast" data-status="info">
<span class="sk-alert-icon">
<svg class="sk-icon" viewBox="0 0 24 24" aria-hidden="true" focusable="false">
<circle cx="12" cy="12" r="9">
</circle>
<path d="M12 11v5">
</path>
<path d="M12 8h.01">
</path>
</svg>
</span>
<div class="sk-alert-content">
Draft saved automatically.
</div>
</div>
</div>Pending states, without htmx running
This page serves no JavaScript, so the .htmx-request class is written onto these examples by hand. That is also how you develop against the adapter: the class is the whole contract, and you can style-test it without the library loaded.
idle.htmx-request - what the adapter doesaria-busy - what the core does, framework-independentShow markup
<div class="sk-flex sk-flex-col sk-gap-5">
<div class="sk-flex sk-flex-col sk-gap-2">
<code class="sink-caption">
idle
</code>
<div class="sk-flex sk-items-center sk-gap-3">
<button class="sk-button" type="button" data-variant="solid" data-size="md">
Save
<span class="sk-indicator">
<span class="sk-spinner" data-motion="essential" aria-hidden="true">
</span>
</span>
</button>
</div>
</div>
<div class="sk-flex sk-flex-col sk-gap-2">
<code class="sink-caption">
.htmx-request - what the adapter does
</code>
<div class="sk-flex sk-items-center sk-gap-3 htmx-request">
<button class="sk-button" type="button" data-variant="solid" data-size="md" class="sk-button htmx-request">
Save
<span class="sk-indicator">
<span class="sk-spinner" data-motion="essential" aria-hidden="true">
</span>
</span>
</button>
</div>
</div>
<div class="sk-flex sk-flex-col sk-gap-2">
<code class="sink-caption">
aria-busy - what the core does, framework-independent
</code>
<button class="sk-button" type="button" data-variant="solid" data-size="md" aria-busy="true" disabled>
<span class="sk-spinner" data-motion="essential" aria-hidden="true">
</span>
Saving
</button>
</div>
</div>Spinner
A spinner alone is a poor status message - it says something is happening but never what, and never ends for someone who cannot see it. Pair it with text.
Show markup
<div class="sk-flex sk-items-center sk-gap-5">
<span class="sk-flex sk-items-center sk-gap-2">
<span class="sk-spinner" data-motion="essential" aria-hidden="true">
</span>
Loading
</span>
<span class="sk-flex sk-items-center sk-gap-2">
<span class="sk-spinner" data-motion="essential" aria-hidden="true" data-scale="lg">
</span>
Loading
</span>
<span class="sk-flex sk-items-center sk-gap-2">
<span class="sk-spinner" data-motion="essential" aria-hidden="true" data-scale="xl">
</span>
Loading
</span>
</div>Wiring, end to end
Once, in the layout:
<link rel="stylesheet" href={ ds.StylesheetHref() }/>
<link rel="stylesheet" href={ ds.HTMXStylesheetHref() }/>
...
@components.ToastRegion("")Then append a toast to any response that should produce one:
templ SaveResult(saved bool) {
<form>...</form>
if saved {
@components.ToastOOB(components.ToastProps{
Status: components.StatusSuccess,
Title: "Saved",
AutoHide: true,
}) {
Your changes are live.
}
}
}No HX-Trigger header, no event listener, no JavaScript from this design system. htmx puts the rendered markup into the region that is already on the page.
Show markup
<div class="sk-flex sk-flex-col sk-gap-4">
<p>
Once, in the layout:
</p>
<pre>
<code>
<link rel="stylesheet" href={ ds.StylesheetHref() }/>
<link rel="stylesheet" href={ ds.HTMXStylesheetHref() }/>
...
@components.ToastRegion("")
</code>
</pre>
<p>
Then append a toast to any response that should produce one:
</p>
<pre>
<code>
templ SaveResult(saved bool) {
<form>...</form>
if saved {
@components.ToastOOB(components.ToastProps{
Status: components.StatusSuccess,
Title: "Saved",
AutoHide: true,
}) {
Your changes are live.
}
}
}
</code>
</pre>
<p>
No HX-Trigger header, no event listener, no JavaScript from this design system. htmx puts the rendered markup into the region that is already on the page.
</p>
</div>