Toast

Stacking notification surface. A fixed-position .toast-container holds one or more .toast items, each fading in with a CSS-only opacity transition. Six placement variants anchor the stack to any viewport edge. Combinations and their light pairs carry semantic colour. JavaScript still owns push, dismiss, and timer logic.

Live

Default (top-end placement, default combination)

Saved
Your draft was saved a moment ago.
<div class="toast-container" aria-live="polite" aria-atomic="false">
  <div class="toast is-show" role="status">
    <div class="toast__header">
      <span class="toast__title">Saved</span>
      <button type="button" class="toast__dismiss" aria-label="Dismiss">&times;</button>
    </div>
    <div class="toast__body">Your draft was saved a moment ago.</div>
  </div>
</div>
@use '@adnap/krysalicss' with (
  $feature-list: ('base-reset', 'base-global', 'module-toast')
);
Playground

.is-success at .is-bottom-start

Deployed
Build #482 is live on production.
<div class="toast-container is-bottom-start" aria-live="polite">
  <div class="toast is-success is-show" role="status">
    <div class="toast__header">
      <span class="toast__title">Deployed</span>
      <button type="button" class="toast__dismiss" aria-label="Dismiss">&times;</button>
    </div>
    <div class="toast__body">Build &#35;482 is live on production.</div>
  </div>
</div>
@use '@adnap/krysalicss' with (
  $feature-list: ('base-reset', 'base-global', 'module-toast')
);
Playground

.is-info.is-light at .is-bottom-center

FYI
Light variant of the info combination.
<div class="toast-container is-bottom-center" aria-live="polite">
  <div class="toast is-info is-light is-show" role="status">
    <div class="toast__header">
      <span class="toast__title">FYI</span>
      <button type="button" class="toast__dismiss" aria-label="Dismiss">&times;</button>
    </div>
    <div class="toast__body">Light variant of the info combination.</div>
  </div>
</div>
@use '@adnap/krysalicss' with (
  $feature-list: ('base-reset', 'base-global', 'module-toast')
);
Playground

.is-warning at .is-top-center

Heads up
Top-center placement.
<div class="toast-container is-top-center" aria-live="polite">
  <div class="toast is-warning is-show" role="status">
    <div class="toast__header">
      <span class="toast__title">Heads up</span>
      <button type="button" class="toast__dismiss" aria-label="Dismiss">&times;</button>
    </div>
    <div class="toast__body">Top-center placement.</div>
  </div>
</div>
@use '@adnap/krysalicss' with (
  $feature-list: ('base-reset', 'base-global', 'module-toast')
);
Playground

Markup

.toast-container is position: fixed and pointer-events: none so clicks pass through the empty gutter between toasts; each .toast re-enables pointer events on itself so its dismiss button remains interactive. Without a placement modifier the container anchors to the viewport's top inline-end corner.

markup
<div class="toast-container" aria-live="polite" aria-atomic="false">
  <div class="toast is-show" role="status">
    <div class="toast__header">
      <span class="toast__title">Saved</span>
      <button type="button" class="toast__dismiss" aria-label="Dismiss">&times;</button>
    </div>
    <div class="toast__body">Your draft was saved a moment ago.</div>
  </div>
</div>

The opacity transition starts at opacity: 0. Consumer JS adds .is-show once the toast is in the DOM to fade it in; removing the class fades it out before teardown. The native hidden attribute collapses the toast out of layout entirely (no transition).

Placements

Each modifier rebinds the container's inset-block-* and inset-inline-* logical properties so the stack moves correctly under dir="rtl". Top-center and bottom-center additionally apply transform: translateX(-50%) to centre the column on the inline axis.

ModifierAnchorInner alignment
none (and .is-top-end)Top inline-end corner.flex-end.
.is-top-startTop inline-start corner.flex-start.
.is-top-centerTop edge, inline-centred via translateX(-50%).center.
.is-bottom-startBottom inline-start corner.flex-start.
.is-bottom-centerBottom edge, inline-centred via translateX(-50%).center.
.is-bottom-endBottom inline-end corner.flex-end.

States

HookEffectNotes
bare .toastopacity: 0 — present in layout but invisible.Lets consumer JS pre-mount the toast before triggering the reveal.
.is-showopacity: 1 via the fade transition.Add on mount-and-ready, remove to fade out. Same role in the CSS-only path: the markup ships with the class already set.
[hidden]display: none — removed from layout entirely.Native attribute. Skips the transition. Use for instant teardown via JS.
:has(.toast__toggle:checked)display: none — removed from layout when the paired checkbox is checked.CSS-only dismiss. The toast hosts a hidden <input type="checkbox" class="toast__toggle">; a <label class="toast__dismiss" for=…> toggles it on click.

The transition itself is stripped under prefers-reduced-motion: reduce — a toast appearing instantly is the accessible default for vestibular-sensitive users (WCAG 2.3.3).

Combinations

Every shared combination (is-primary, is-success, is-warning, is-danger, is-info, …) applies via combinations-apply(). Each one also ships a .is-light companion via combinations-apply-light() — chain both classes (.toast.is-info.is-light) for the desaturated variant. The light pair preserves the semantic colour cue while reducing contrast intensity, useful for low-stakes notifications.

.is-info.is-light
<div class="toast-container is-bottom-center" aria-live="polite">
  <div class="toast is-info is-light is-show" role="status">
    <div class="toast__header">
      <span class="toast__title">FYI</span>
      <button type="button" class="toast__dismiss" aria-label="Dismiss">&times;</button>
    </div>
    <div class="toast__body">Light variant of the info combination.</div>
  </div>
</div>

Forced-colors mode strips background-color and box-shadow; a CanvasText border preserves the toast's silhouette so users on Windows High Contrast still perceive it as a discrete surface.

Reveal & dismiss mechanisms

Toasts ship with two parallel reveal/dismiss paths, gated by the framework-wide $global-controls list ('css', 'js' — both emit by default). Strip whichever you don't need to shrink the slice.

CSS-only path (zero JS)

A hidden <input type="checkbox" class="toast__toggle"> sits inside the toast; a <label class="toast__dismiss" for=…> is the visible close glyph. Clicking the label flips the checkbox, the :has(toggle:checked) rule collapses the toast out of layout.

CSS-only dismiss (input checkbox + label)

Saved
Click the × to dismiss. Zero JS.
<div class="toast-container" aria-live="polite">
  <div class="toast is-show" role="status">
    <input type="checkbox" id="toast-css-1" class="toast__toggle" hidden>
    <div class="toast__header">
      <span class="toast__title">Saved</span>
      <label for="toast-css-1" class="toast__dismiss" aria-label="Dismiss">&times;</label>
    </div>
    <div class="toast__body">Click the × to dismiss. Zero JS.</div>
  </div>
</div>
@use '@adnap/krysalicss' with (
  $feature-list: ('base-reset', 'base-global', 'module-toast')
);
Playground
CSS-only markup
<div class="toast-container" aria-live="polite">
  <div class="toast is-show" role="status">
    <input type="checkbox" id="toast-css-1" class="toast__toggle" hidden>
    <div class="toast__header">
      <span class="toast__title">Saved</span>
      <label for="toast-css-1" class="toast__dismiss" aria-label="Dismiss">&times;</label>
    </div>
    <div class="toast__body">Click the × to dismiss. Zero JS.</div>
  </div>
</div>

The label inherits the same chrome as the <button> variant because both share the .toast__dismiss selector. Focus on the hidden checkbox re-projects onto the label through :has(.toast__toggle:focus-visible) so keyboard navigation still paints a visible ring.

JS-driven path

Consumer JS owns the lifecycle: mount the toast invisibly, add .is-show to fade it in, run a timer, dismiss by setting [hidden] or removing .is-show. The dismiss control is a real <button>.

JS-driven dismiss (button + hidden attribute)

Saved
Click the × to dismiss. JS-driven.
<div class="toast-container" aria-live="polite">
  <div class="toast is-show" role="status" data-toast>
    <div class="toast__header">
      <span class="toast__title">Saved</span>
      <button type="button" class="toast__dismiss" aria-label="Dismiss"
              onclick="this.closest('.toast').hidden = true">&times;</button>
    </div>
    <div class="toast__body">Click the × to dismiss. JS-driven.</div>
  </div>
</div>
@use '@adnap/krysalicss' with (
  $feature-list: ('base-reset', 'base-global', 'module-toast')
);
Playground
JS-driven markup
<div class="toast-container" aria-live="polite">
  <div class="toast is-show" role="status" data-toast>
    <div class="toast__header">
      <span class="toast__title">Saved</span>
      <button type="button" class="toast__dismiss" aria-label="Dismiss"
              onclick="this.closest('.toast').hidden = true">&times;</button>
    </div>
    <div class="toast__body">Click the × to dismiss. JS-driven.</div>
  </div>
</div>

A 20–30 line consumer wrapper covers push, timer, pause-on-hover, and the dismiss handler. Krysalicss intentionally does not ship that runtime — JS-free is a product invariant for the framework itself.

Stripping a branch

@use '@adnap/krysalicss/module/toast' with (
  $controls: ('css',),  // drop the JS hooks
);

Variables

VariableDefaultNotes
$selector'.toast'Toast surface.
$container-selector'.toast-container'Fixed-position stack wrapper.
$header-selector'.toast__header'Title + dismiss row.
$title-selector'.toast__title'Bold heading slot.
$body-selector'.toast__body'Main content slot.
$dismiss-selector'.toast__dismiss'Bare-glyph dismiss control. Shared selector across the JS <button> and the CSS-only <label> — UA chrome reset; opacity 0.6 idle, 1 on hover / focus-visible.
$toggle-selector'.toast__toggle'Hidden <input type="checkbox"> that drives the CSS-only dismiss. Paired with a <label for=…> reusing $dismiss-selector.
$show-modifier'.is-show'Reveal hook. JS adds it post-mount; CSS-only markup ships with it set.
$top-start-modifier'.is-top-start'Container anchor — top inline-start corner, items aligned to flex-start.
$top-center-modifier'.is-top-center'Container anchor — top edge, inline-centred.
$top-end-modifier'.is-top-end'Container anchor — top inline-end corner (same as the default; explicit hook for theme switching).
$bottom-start-modifier'.is-bottom-start'Container anchor — bottom inline-start corner.
$bottom-center-modifier'.is-bottom-center'Container anchor — bottom edge, inline-centred.
$bottom-end-modifier'.is-bottom-end'Container anchor — bottom inline-end corner.
$max-width420pxPer-toast and container max-width.
$gap$size-extra-smallStack gap between toasts and inline gap inside .toast__header.
$padding-y$size-smallVertical padding inside header / body slots.
$padding-x$size-normalHorizontal padding inside header / body slots, and container inset.
$border-radius$global-border-radiusToast surface radius.
$z-index1090Stacking order. Above tooltip (1080), popover (1070), modal (1055).
$shadow0 6px 20px rgb(0 0 0 / 0.15)Drop shadow tuned to read across light / dark themes.
$transition-duration$global-transition-durationOpacity transition. Stripped under prefers-reduced-motion: reduce.
$default-combination$color-combination-defaultBg/fg tuple used when no .is-* modifier is present.

See Combinations and light variants for $enable-light, $default-combination, and $controls — cross-cutting plumbing common to every theme-aware component.

Override example

app.scss
@use '@adnap/krysalicss/module/toast' with (
  $max-width: 480px,
  $z-index: 1100,
  $shadow: 0 12px 32px rgb(0 0 0 / 0.18),
);

Tokens consumed

TokenUsed for
--kc-toast-bg / --kc-toast-fgDefault toast surface. Falls back through --kc-bg / --kc-fg so the toast tracks the active theme.
--kc-{label}-bg / --kc-{label}-fgEach combination modifier (.toast.is-primary, .toast.is-success, …).
--kc-{label}-light-bg / --kc-{label}-light-fgDesaturated companion paired via .toast.is-<label>.is-light.
--kc-focus-ringDismiss button focus outline. Owned by helper/controls.

Accessibility

  • Wrap the container with aria-live="polite" (or "assertive" for critical alerts) and role="status" / role="alert" on each toast so screen readers announce arrivals.
  • The dismiss button must carry an accessible name — aria-label="Dismiss" or visible text — since the glyph alone is invisible to AT.
  • Auto-dismiss timers should respect prefers-reduced-motion and pause on hover / focus so keyboard users can read long messages without the toast vanishing.