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)
<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">×</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')
); .is-success at .is-bottom-start
<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">×</button>
</div>
<div class="toast__body">Build #482 is live on production.</div>
</div>
</div> @use '@adnap/krysalicss' with (
$feature-list: ('base-reset', 'base-global', 'module-toast')
); .is-info.is-light at .is-bottom-center
<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">×</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')
); .is-warning at .is-top-center
<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">×</button>
</div>
<div class="toast__body">Top-center placement.</div>
</div>
</div> @use '@adnap/krysalicss' with (
$feature-list: ('base-reset', 'base-global', 'module-toast')
); 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.
<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">×</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.
| Modifier | Anchor | Inner alignment |
|---|---|---|
none (and .is-top-end) | Top inline-end corner. | flex-end. |
.is-top-start | Top inline-start corner. | flex-start. |
.is-top-center | Top edge, inline-centred via translateX(-50%). | center. |
.is-bottom-start | Bottom inline-start corner. | flex-start. |
.is-bottom-center | Bottom edge, inline-centred via translateX(-50%). | center. |
.is-bottom-end | Bottom inline-end corner. | flex-end. |
States
| Hook | Effect | Notes |
|---|---|---|
bare .toast | opacity: 0 — present in layout but invisible. | Lets consumer JS pre-mount the toast before triggering the reveal. |
.is-show | opacity: 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.
<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">×</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)
<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">×</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')
); <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">×</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)
<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">×</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')
); <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">×</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
| Variable | Default | Notes |
|---|---|---|
$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-width | 420px | Per-toast and container max-width. |
$gap | $size-extra-small | Stack gap between toasts and inline gap inside .toast__header. |
$padding-y | $size-small | Vertical padding inside header / body slots. |
$padding-x | $size-normal | Horizontal padding inside header / body slots, and container inset. |
$border-radius | $global-border-radius | Toast surface radius. |
$z-index | 1090 | Stacking order. Above tooltip (1080), popover (1070), modal (1055). |
$shadow | 0 6px 20px rgb(0 0 0 / 0.15) | Drop shadow tuned to read across light / dark themes. |
$transition-duration | $global-transition-duration | Opacity transition. Stripped under prefers-reduced-motion: reduce. |
$default-combination | $color-combination-default | Bg/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
@use '@adnap/krysalicss/module/toast' with (
$max-width: 480px,
$z-index: 1100,
$shadow: 0 12px 32px rgb(0 0 0 / 0.18),
); Tokens consumed
| Token | Used for |
|---|---|
--kc-toast-bg / --kc-toast-fg | Default toast surface. Falls back through --kc-bg / --kc-fg so the toast tracks the active theme. |
--kc-{label}-bg / --kc-{label}-fg | Each combination modifier (.toast.is-primary, .toast.is-success, …). |
--kc-{label}-light-bg / --kc-{label}-light-fg | Desaturated companion paired via .toast.is-<label>.is-light. |
--kc-focus-ring | Dismiss button focus outline. Owned by helper/controls. |
Accessibility
- Wrap the container with
aria-live="polite"(or"assertive"for critical alerts) androle="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-motionand pause on hover / focus so keyboard users can read long messages without the toast vanishing.