Skip to content

Toast

The element is the container, not the message. You add it once in the app and call show() when there is something to report.

The container is a role="status" region, not role="alert". A message that appears in the corner should not interrupt what the screen reader is doing. If the message is important enough to interrupt, it belongs in an Alert in the page itself.

// main.ts, runs once when the app starts
import { defineLtToast } from "@lo-tek/interfaces/toast"
import "@lo-tek/interfaces/tokens.css"
import "@lo-tek/interfaces/toast.css"
defineLtToast()

The container is added once, and the messages come from your code. That is the difference from the other components: here you call a method instead of setting an attribute.

A toast has three parts, and lt.toast() gives one attribute set for each. It is the same toasts as in the React and Astro recipes above:

const toasts = lt.toast({ label: "Messages" })
SetWhich partWho writes it
toasts.hostThe container, that is <lt-toast>You, once in the app
toasts.messageThe message itselfThe component, when you call show()
toasts.closeThe close button in the messageThe component

That is why you spread host and not the whole object. The container is the only element you write:

<lt-toast {...toasts.host} />

If you spread the whole of toasts, you set three attributes called host, message and close, none of which the browser knows. The container then has no role="status" until defineLtToast() has run, because the component fills in the role when it is missing. Until then no screen reader announces what appears in it, and data-ignore-morph never arrives, since that is the one thing the component does not set itself.

The other two sets are names, not markup. The component knows them and uses them on what it creates itself. You need them only when someone else writes the message: a Datastar app where the server patches a message straight into the region has to write lt-toast__message and lt-toast__close itself, and then they should have the same names as the ones the component uses.

Without a build step, you write the same set out as plain attributes, as the “Plain HTML” and “Datastar” recipes do:

<lt-toast id="messages" class="lt-toast" role="status" aria-live="polite"
aria-atomic="false"
aria-label="Messages" data-ignore-morph></lt-toast>

data-ignore-morph is the most important of them, and the only one the component does not set itself.

data-ignore-morph is not optional. The component creates and removes the messages inside the region itself, and without the attribute an update from the server tears them away, because the server’s version of the region is empty. See Markup and behaviour.

NameWhat it is
durationDefault lifetime in milliseconds. Without the attribute it is 6000. 0 leaves the messages in place.
labelText that says what the region is. Becomes aria-label. Default: “Notifications”
close-labelWhat the close button in each message is called for the screen reader. Default: “Close message”. lt.toast({ closeLabel }) writes it
show(text, options)Shows a message and returns the element
dismiss(message)Removes one message, and dispatches toast-dismiss. If the message had focus, focus moves to the message next to it
clear()Removes all of them, without dispatching anything

showToast(element, text, options) from @lo-tek/interfaces/toast does the same as show(), but first waits until the element is registered. Use it where the call can come before defineLtToast(), as in a module loaded alongside the registration.

The options for show():

OptionDefaultEffect
colorneutralsuccess, warning or danger gives a coloured border
durationfrom the elementMilliseconds before the message disappears
closeLabelfrom close-labelWhat the close button is called for the screen reader, for this one message

Both texts can be translated. See Translation.

A message that disappears by itself is an accessibility trap: someone who reads slowly, uses magnification or a screen reader may not have time to read it. The component does two things about that:

  • The close button is always there.
  • The timer stops while the mouse is over the message or focus is inside it, and only runs again when both are outside.

If the message needs to be readable later, it does not belong here. A receipt belongs in the page.

ClassWhat it is
.lt-toastThe region the messages are in, that is the host. It carries the placement in the corner
.lt-toast__messageOne message. The colour comes from data-color.
.lt-toast__closeThe close button
VariableDefault
--lt-toast-spacevar(--lt-spacing-4)
--lt-toast-widthmin(24rem, calc(100vw - var(--lt-spacing-8)))
--lt-toast-paddingvar(--lt-spacing-3) var(--lt-spacing-4)
--lt-toast-radiusvar(--lt-spacing-1)
--lt-toast-layer60

The container lets clicks through when it is empty, so it does not block buttons underneath it.

See Customising for how the variables and the layer work.

  • One thing at a time. Five messages on top of each other are read out one after another, and none of them gets attention.
  • Do not put important actions in a message that disappears. The undo button belongs in the page.
  • Errors that stop the user belong in an Alert or an Error Summary, not in the corner.
  • The text should say what happened, not just “Done”.
  • Not while a modal dialog is open. Everything outside a modal dialog is inert: the message sits under the backdrop, the close button cannot be pressed, and the screen reader probably does not announce it. If the message concerns something the user is doing in the dialog, show it in the dialog, for example with an Alert, or wait until the dialog is closed.