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 timeout at 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.Viewport is a role="region" landmark, named with the count of toasts. F6 moves the focus into it from anywhere on the page, and back. A Tab past 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, and role="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.Root is a role="dialog" that is not modal, or an alertdialog for the high priority. It has aria-labelledby from the title and aria-describedby from 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.
  • Escape closes the focused toast. The focus moves to the next toast, or back to where it was.
  • The viewport stays reachable behind a modal Dialog: F6 moves the focus into it, Tab moves in it, and a Tab past its last button goes back to the dialog.

API reference

Provider

The list of toasts and the timers. It renders nothing of its own.

Prop Type Default

* 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.

Prop Type Default

* required. Native HTML attributes of the underlying element are also accepted.

Data attribute Description
data-expanded Present while the pointer rests on the viewport, or the focus is in it. A tap on a toast, not a swipe, holds it on until a touch outside the viewport.
data-toast-viewport Identifies the viewport element.

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.

Prop Type Default

* required. Native HTML attributes of the underlying element are also accepted.

Data attribute Description
data-anchored Present when the item has an anchor.
data-placement The side of the anchor where the toast sits, after a flip.
data-toast-positioner Identifies the positioner element.

Root

One toast. It makes a div with role="dialog", or role="alertdialog" for a high priority, that is not modal.

Prop Type Default

* required. Native HTML attributes of the underlying element are also accepted.

Data attribute Description
data-anchored Present when the item has an anchor. The toast is out of the stack.
data-ending Present through the exit animation.
data-entering Present through the enter animation.
data-exiting Present through the exit animation.
data-expanded Present while the pointer rests on the viewport, or the focus is in it. A tap on a toast, not a swipe, holds it on until a touch outside the viewport.
data-front Present on the one toast in front. A toast on its way out drops it, and it keeps the place it had for its exit.
data-limited Present on a toast past the limit. It is inert, and it waits in the place behind the stack.
data-priority The priority of the item: `low` or `high`.
data-swipe-direction The side of the swipe in progress, or of the swipe that dismissed the toast.
data-swipe-dismissed Present when a swipe dismissed the toast, for the exit.
data-swiping Present while a swipe is in progress.
data-toast-id The id of the item.
data-toast-root Identifies the toast element.
data-type The `type` of the item, such as `success` or `loading`.

Content

The message: the title and the description. Keep the buttons out of it.

Prop Type Default

* required. Native HTML attributes of the underlying element are also accepted.

Data attribute Description
data-priority The priority of the item: `low` or `high`.
data-toast-content Identifies the content element.
data-type The `type` of the item, such as `success` or `loading`.

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.

Prop Type Default

* required. Native HTML attributes of the underlying element are also accepted.

Data attribute Description
data-toast-title Identifies the title element.

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.

Prop Type Default

* required. Native HTML attributes of the underlying element are also accepted.

Data attribute Description
data-toast-description Identifies the description element.

Close

A button that closes the toast.

Prop Type Default

* required. Native HTML attributes of the underlying element are also accepted.

Data attribute Description
data-toast-close Identifies the close button.

Action

The one thing the toast offers. The press closes the toast.

Prop Type Default

* required. Native HTML attributes of the underlying element are also accepted.

Data attribute Description
data-toast-action Identifies the action button.