Toast
A passing notice in a corner of the screen, for what just happened: saved, sent, didn't work. Stacks up, pauses when the pointer or focus enters, and is announced by screen readers. For what stays true while the person is on the screen, use the alert.
Examples
Each type has its own color, icon and duration. The error stays longer because the person needs to read it and, often, act; loading doesn't close on its own, because what ends it is the end of the operation.
Types
info, success, warning, error and loading.
Doesn't close on its own
duration: null keeps it on screen until someone clicks the X.
Custom duration
duration in milliseconds, instead of the type's default.
Stack
Up to three visible, stacked; the pointer or focus fans them out. The fifth closes the oldest.
| Type | Stays on screen | Announced as |
|---|---|---|
info | 4 s | role="status", polite region |
success | 3.5 s | role="status", polite region |
warning | 6 s | role="status", polite region |
error | 8 s | role="alert", assertive region |
loading | until the operation ends | role="status", polite region |
In the toast, red is the error type. In the alert and badge classes, the same tone is called
is-danger.
How to use
There's nothing to mark up in the HTML: the toast is called when something happens. The container for each position is created the first time it's used.
Tucano.toast('Saved'); // info
Tucano.toast.success('Record saved');
Tucano.toast.error('Couldn\'t save', { title: 'Error 500' });
Tucano.toast({
type: 'success',
title: 'Contract deleted',
text: 'You can still undo it.',
action: { text: 'Undo', onClick: (t) => restore() },
position: 'bottom-end',
duration: null, // doesn't close on its own
closable: true, // the X
});The shortcuts info, success, warning, error and
loading take the text and then any other option. The action is a system button: it calls
onClick with the toast and closes. Everything returns the instance, which is the same as new Tucano.Toast({ ... })
— it takes only the options, no target.
const t = Tucano.toast('Exporting...');
t.close();
// The event fires from the card itself after it leaves the screen; it doesn't bubble.
t.node.addEventListener('tucano:toast-closed', () => console.log('closed'));Loading and promises
The loading toast turns into the result on the same card, instead of closing one and opening another:
the stack doesn't reshuffle and the eye doesn't lose track of the notice it was already reading.
// Manual control: keep the instance and change whatever you need.
const t = Tucano.toast.loading('Saving contract...');
await save();
t.update({ type: 'success', text: 'Contract saved' });
// Or hand over the promise and let it drive the three states.
Tucano.toast.promise(fetch(url), {
loading: 'Sending...',
success: (response) => `Sent (${response.status})`,
error: 'Couldn\'t send',
position: 'top-center', // the rest applies to the toast
});update() changes type, title, text and action without recreating the card. When the type changes without
duration, the new type's duration applies, and the clock restarts: without that, loading, which doesn't close, would turn into
a "saved" stuck on screen forever.
In toast.promise(), success and error accept text or a function that receives the
result or the error. Without them, the library's default texts are used: "Carregando...", "Pronto" and "Algo deu errado" in Portuguese, or whatever you set for the whole page with Tucano.setTexts({ toast: { loading, success, error } }). The function returns the same
promise it received, so it doesn't get in the way of code already chaining on it — if rejected, it stays rejected, and the error is
handled wherever it would have been.
null means "don't close", not "use the default"
The default duration is resolved with in, not with ??. Loading's null is a value: with ?? it would fall back to the 4-second default, and the toast would vanish in the middle of the operation.
Positions
Six corners and centers, with bottom-end as the default. Each position has its own stack.
On phones, below 40rem, the toast spans the screen width and enters along the vertical axis, whatever the position: pinned to a corner it would have different margins on each side, and sliding a card as wide as the screen horizontally is too long a trip for a short notice. The edge respects the iPhone's home indicator and the camera notch.
max limits the open toasts in the same position, 4 by default; going over closes the oldest. In the stack,
those past the third fade from view, because a stack of ten helps no one.
Django messages
The messages framework becomes toasts without a line of JavaScript. Put the loop once, in the
base template: each <div> becomes a toast and leaves the page.
{% for m in messages %}
<div data-tuc-toast data-type="{{ m.level_tag }}">{{ m }}</div>
{% endfor %}messages.success(request, "Contract saved")
return redirect("contracts")| Attribute | What it's for |
|---|---|
data-tuc-toast | Marks the element; its text becomes the toast's text |
data-type | The type. debug becomes info; info, success, warning and error pass straight through |
data-title | Title |
data-text | Text, instead of the element's content |
data-duration | Milliseconds; false doesn't close on its own |
data-position | One of the six positions |
m.level_tag, not m.tags, if you use extra_tags
Only the first word of data-type counts. Django's m.tags writes the extra_tags before the level, and then the first word is no longer the type. Without extra_tags, both give the same result.
Through the HTMX header
In an HTMX response there's no redirect and no base template for the messages loop. The server triggers the
toast through the HX-Trigger header, and the CDN script is already listening.
import json
def save(request, pk):
...
return HttpResponse(headers={
"HX-Trigger": json.dumps({
"tucano:toast": {"type": "success", "text": "Contract saved"},
}),
})The detail accepts any toast option, or just a text: {"tucano:toast": "Saved"}. HTMX fires the
tucano:toast event on the element that made the request, it bubbles up to <body>, and there it becomes a toast.
The same event works from any other code:
document.body.dispatchEvent(new CustomEvent('tucano:toast', {
detail: { type: 'success', text: 'Contract saved' },
}));With the CDN, listenForEvents() is turned on together with initialization. When importing tucano from
npm nothing runs on its own: call listenForEvents(), or import tucano/auto.
Accessibility
The toast doesn't steal focus: it notifies without pulling the person away from what they were doing. The screen reader is what announces it.
The region is created before the message
aria-live sits on the container, not on the toast. A live region has to exist in the DOM before the content arrives; if it's created together with the message, the screen reader doesn't announce it. Errors speak in an assertive region and everything else in a polite one, and both share the same stage: positioned separately, they would become two parallel stacks on screen and the limit would count double.
When update() changes a toast to error, or from error to another type, it switches
regions, and that's what makes the screen reader announce the change. On screen nothing moves, because the position comes from the stage.
The clock pauses when the pointer enters the toast or something inside it receives focus: nobody can read something that disappears
while they're trying to click "Undo". For the same reason the stack fans out on focus too, not just on pointer. The X
is a real button, with aria-label="Fechar" (the library's default label, in Portuguese until you change it with Tucano.setTexts({ toast: { close } })). With prefers-reduced-motion, entering and leaving become
just a fade.
API
Generated from the code on every build — if something is not here, it does not exist.
[data-tuc-toast]new Tucano.Toast(opcoes) Tucano.toast() Tucano.listenForEvents()data-duration data-position data-text data-title data-typeupdate closetucano:toast-closedOptions
The notes in this table come from comments in the source code, which are written in Portuguese.
| Option | Default | What for |
|---|---|---|
type | 'info' | 'info' | 'success' | 'warning' | 'error' | 'loading' |
title | null | |
text | '' | |
duration | undefined | ms. null nao fecha sozinho. Padrao depende do tipo |
position | 'bottom-end' | top-start|top-center|top-end|bottom-start|bottom-center|bottom-end |
closable | true | |
action | null | { text, onClick } |
max | 4 | toasts simultaneos na mesma posicao |