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>
AttributeTypeDefaultNotes
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>
AttributeTypeDefaultNotes
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-durationtime5sHow 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.
Draft saved automatically.
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 does
aria-busy - what the core does, framework-independent
Show 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.

Loading Loading Loading
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>
      &lt;link rel=&#34;stylesheet&#34; href={ ds.StylesheetHref() }/&gt;
&lt;link rel=&#34;stylesheet&#34; href={ ds.HTMXStylesheetHref() }/&gt;
...
@components.ToastRegion(&#34;&#34;)
    </code>
  </pre>
  <p>
    Then append a toast to any response that should produce one:
  </p>
  <pre>
    <code>
      templ SaveResult(saved bool) {
    &lt;form&gt;...&lt;/form&gt;
    if saved {
        @components.ToastOOB(components.ToastProps{
            Status:   components.StatusSuccess,
            Title:    &#34;Saved&#34;,
            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>