Toast
Toast shows a short message about an event, for a time, without a stop of the work. The provider holds the list, and the viewport is the landmark where the toasts land. A screen reader hears each message from a live region beside them.
Anatomy
Toast.Provider holds the list of toasts and the timers. Put one around the app. Toast.Viewport is the region where the toasts land. Put one in the provider, and give it a snippet that renders a Toast.Root for each item.
Toast.Content holds Toast.Title and Toast.Description. Toast.Action and Toast.Close are the buttons, outside the content. Toast.Positioner is optional: it puts a toast with an anchor against that element.
<script>
import { Toast } from '@human-kit/ui';
</script>
<Toast.Provider>
<App />
<Toast.Viewport>
{#snippet children(toast)}
<Toast.Root {toast}>
<Toast.Content>
<Toast.Title />
<Toast.Description />
</Toast.Content>
<Toast.Close aria-label="Close">x</Toast.Close>
</Toast.Root>
{/snippet}
</Toast.Viewport>
</Toast.Provider>The manager
Call useToastManager() in a component under the provider, or use bind:manager on the provider. The manager has add, update, close and promise.
add takes title, description, type, timeout, priority and data, and it answers the id of the toast. With the id of a toast that is on the screen, add updates it. update replaces the fields it names, and it gives the toast its full time again. close closes one toast, or all of them without an id.
<script>
import { useToastManager } from '@human-kit/ui';
const toasts = useToastManager();
</script>
<button onclick={() => toasts.add({ title: 'Saved', description: 'The copy is on the server.' })}>
Save
</button>The title and the description are text. That text is what the screen reader hears. Toast.Title and Toast.Description show it without children, and your own children replace it on the screen.
A promise
promise adds a loading toast, which stays until the promise settles. It then turns into success or error, with the text for each state, and the timer starts.
An action
Toast.Action is the one thing the toast offers, such as Undo. The press closes the toast, because the offer is taken. keepOpen holds it for an action that changes the toast instead.
Timers
A toast stays timeout milliseconds: 5000 by default, on the provider or on the toast. 0 keeps it until a close.
The timers stop while the pointer rests on the viewport, and while the keyboard focus is in it. A touch has no hover: a tap on a toast holds the stack open, and the timers with it, until a touch outside the viewport. A tap lifts where it landed. A swipe is not a tap, and a press on a button is not one. They also stop while the tab is hidden. They start again with the time each toast had left. A toast that closes while the user reads it is a toast the user did not read.
The stack
The viewport shows the newest toasts up to limit: 3 by default. The older ones wait behind, hidden and inert, with their timers stopped, and they come forward as the newer ones close.
Each toast gets --toast-index, --toast-offset-y and --toast-height, and the viewport gets --toast-frontmost-height. data-front marks the one toast in front. A toast on its way out drops it, and it keeps the place it had. data-expanded is on the viewport and on each toast while the pointer rests on the viewport, or while the focus is in it. A tap on a toast holds it on until a touch outside the viewport. The viewport of the first demo stacks the toasts in the corner with them, and spreads them out on data-expanded. Its source is in viewport.svelte, under the demo.
.toast {
position: absolute;
inset-block-end: 0;
transform: translateY(calc(var(--toast-index) * -12px)) scale(calc(1 - var(--toast-index) * 0.05));
}
.toast[data-expanded] {
transform: translateY(calc(-1 * var(--toast-offset-y) - var(--toast-index) * 8px));
}A toast that closes is out of the stack at once: the ones behind it move up while it fades. It keeps the --toast-index and --toast-offset-y it had, thus its exit runs from its place. A rule that hides the toasts behind must let it pass, with :not([data-ending]).
A toast past the limit waits in the place behind the stack. It comes forward one step when a place frees.
The pointer must not leave the viewport on its way from one toast to the next. The spread stack has a gap between two toasts, and a pointer in the gap is out of all of them. Put a pseudo-element on the toast that covers the gap, as the demo does.
.toast::after {
content: '';
position: absolute;
inset-inline: 0;
inset-block-start: 100%;
height: 8px;
}Motion
data-entering is on the toast through the enter motion, and data-exiting through the exit motion. The toast leaves the DOM when the exit motion ends. Write the enter as an animation on data-entering. The attribute stays on for the whole motion, thus a transition would sit still in the start state. Write the exit as a transition on data-exiting: it is the end state, and the motion runs from where the toast is. Keep the place in the stack in that end state. A transform that reads only translateY(1rem) puts a toast from behind under the front one for its exit.
Move the enter with translate, and leave transform to the place in the stack. A toast that comes in while the next one arrives is pushed back at that moment. With the enter on transform too, the two fight for the property, and the toast jumps to its new place.
.toast {
transform: translateY(calc(var(--toast-index) * -12px));
transition: transform 0.4s;
}
.toast[data-entering] {
animation: toast-in 0.4s ease-out;
}
.toast[data-exiting] {
opacity: 0;
transition: opacity 0.3s;
}
@keyframes toast-in {
from {
opacity: 0;
translate: 0 1rem;
}
}A swipe
A swipe pushes the toast out through one of swipeDirection: bottom and right by default. The toast follows the finger with --toast-swipe-movement-x and --toast-swipe-movement-y, and it comes back when the swipe is short. A pull the other way gives 8 px at most, thus the toast behind stays behind. data-swipe-dismissed and data-swipe-direction are on the toast for the exit.
A swipe never starts on Toast.Close or Toast.Action. Put data-hk-swipe-ignore on other content of your own that must not start one, such as a slider in the toast.
Against an element
Give anchor to add, with an optional placement and offset, for a toast that sits against an element in place of the corner. Put Toast.Positioner around Toast.Root in the viewport snippet. For a toast without an anchor, the positioner is out of the layout, thus one snippet serves both kinds. A toast with an anchor is out of the stack: --toast-index reads 0, and data-anchored is on the root.
Usage guidelines
- Keep the default
timeoutat 5 seconds or more. A user who reads slowly needs the time, and a user with a screen magnifier can miss a short toast. - Use
priority: 'high'only for a message that cannot wait. It interrupts the screen reader. - Put a message the user must act on in a
Dialog, not in a toast. A toast goes away. - Keep the buttons out of
Toast.Content. The announcement reads the content, not the buttons. - Give the app one viewport. Two providers can share one
manager, for example an app and a dialog in it.
Accessibility
Toast.Viewportis arole="region"landmark, named with the count of toasts.F6moves the focus into it from anywhere on the page, and back. ATabpast the last button goes back to where the focus was.- Two live regions beside the viewport announce each toast:
role="status"for the normal priority, androle="alert"for the high priority. They are on the page before the first toast, thus the first toast is announced too. Two toasts in the same tick are two messages. Toast.Rootis arole="dialog"that is not modal, or analertdialogfor the high priority. It hasaria-labelledbyfrom the title andaria-describedbyfrom the description, and it is a tab stop. A toast without a title takes its name from the description: a dialog without a name is a fault.Escapecloses the focused toast. The focus moves to the next toast, or back to where it was.- The viewport stays reachable behind a modal
Dialog:F6moves the focus into it,Tabmoves in it, and aTabpast its last button goes back to the dialog.
API reference
Provider
The list of toasts and the timers. It renders nothing of its own.
* required. Native HTML attributes of the underlying element are also accepted.
Viewport
The region where the toasts land. It makes a div with role="region", at the end of the body, and it renders the snippet for each toast.
* required. Native HTML attributes of the underlying element are also accepted.
Positioner
Puts a toast with an anchor against that element, as a fixed div. For a toast without one, it is out of the layout.
* required. Native HTML attributes of the underlying element are also accepted.
Root
One toast. It makes a div with role="dialog", or role="alertdialog" for a high priority, that is not modal.
* required. Native HTML attributes of the underlying element are also accepted.
Content
The message: the title and the description. Keep the buttons out of it.
* required. Native HTML attributes of the underlying element are also accepted.
Title
The name of the toast. It gives its id to the toast as `aria-labelledby`, and it shows the `title` of the item without children.
* required. Native HTML attributes of the underlying element are also accepted.
Description
The message of the toast. It gives its id to the toast as `aria-describedby`, and it shows the `description` of the item without children.
* required. Native HTML attributes of the underlying element are also accepted.
Close
A button that closes the toast.
* required. Native HTML attributes of the underlying element are also accepted.
Action
The one thing the toast offers. The press closes the toast.
* required. Native HTML attributes of the underlying element are also accepted.