HabToastHost
The bottom-right stack, above everything. It confirms what already happened.
<script setup lang="ts">
const toasts = ref<HabToast[]>([]);
const dismiss = (id: string | number) => {
toasts.value = toasts.value.filter((t) => t.id !== id);
};
</script>
<template>
<HabToastHost
:toasts="toasts"
@dismiss="dismiss"
/>
</template>It never asks a question
A toast disappears. That makes it the wrong place for anything the user has to act on, and the wrong place for anything they might need to read twice — a question is a HabModal, and something that must stay legible is a HabBanner.
Write the message in the past tense. "Session saved", not "Save the session".
The host owns no list
toasts comes in as a prop and entries leave when you drop them in response to dismiss. The queue lives in your app's own store rather than in a module-level singleton this package would hide — one less piece of global state, and testable without mounting anything.
dismiss fires for all three exits: the close button, a swipe, and the timer running out. Nothing is removed for you.
const dismiss = (id: string | number) => {
toasts.value = toasts.value.filter((t) => t.id !== id);
};Same anatomy as the other notices
Tinted surface, tone edge, full-strength tone glyph — shared with HabBanner and HabStatusStrip. What makes a toast a toast is elevation and position, not a different look.
The left edge is --hab-edge-notice, the 4px one. A toast is the definitive case of something that happened and has stopped being true; the 8px --hab-edge-classify would claim it is a standing condition of the region.
Timing
Five seconds by default, per-toast with duration, and Infinity pins one until the user acts. Timers pause while the pointer is over the stack or focus is inside it, which is what stops a toast vanishing mid-read.
The action slot takes one affordance per toast, usually Undo. Give an undo-carrying toast a longer duration than a bare confirmation — the user has to notice it, read it and decide.
Accessibility
- The stack is a named
region, reachable with F8 from anywhere on the page — which is the only way a keyboard user can act on a toast at all. - Each message is announced in a live region.
background: truemarks a toast raised by a background task rather than by something the user just did: it waits its turn instead of interrupting whatever the screen reader is currently saying. Use it for anything the user did not set off themselves.- Swipe-to-dismiss is a pointer convenience; the close button is the accessible path and is always present.
- Not asserted in the specs: auto-dismissal and the hover pause. Both need real timers and real pointer events — the browser test layer's job.
API
Props
| Prop | Type | Default | Description |
|---|---|---|---|
dismissLabel | string | undefined | Accessible name for every toast's dismiss button. Required: Habitus ships no default, so the string is the host application's to translate. |
duration | number | 5000 | Milliseconds each toast stays. Per-toast duration wins. |
label | string | undefined | Announced label for the region, and for each toast. Required: Habitus ships no default, so the string is the host application's to translate. {hotkey} in the region label is substituted for you. |
toasts | HabToast[] | [] | The live stack. Remove an entry when dismiss fires for it. |
Events
| Event | Payload | Description |
|---|---|---|
dismiss | id: string | number | — |
Slots
| Slot | Bindings | Description |
|---|---|---|
action | { toast: HabToast } | One trailing affordance per toast, usually Undo. Receives the toast. |
leading | { toast: HabToast } | Leading adornment per toast, replacing the tone glyph. Receives the toast. |