Tucano v0.37.2

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.

TypeStays on screenAnnounced as
info4 srole="status", polite region
success3.5 srole="status", polite region
warning6 srole="status", polite region
error8 srole="alert", assertive region
loadinguntil the operation endsrole="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")
AttributeWhat it's for
data-tuc-toastMarks the element; its text becomes the toast's text
data-typeThe type. debug becomes info; info, success, warning and error pass straight through
data-titleTitle
data-textText, instead of the element's content
data-durationMilliseconds; false doesn't close on its own
data-positionOne 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.

Markup
[data-tuc-toast]
In JS
new Tucano.Toast(opcoes) Tucano.toast() Tucano.listenForEvents()
Attributes
data-duration data-position data-text data-title data-type
Methods
update close
Events
tucano:toast-closed

Options

The notes in this table come from comments in the source code, which are written in Portuguese.

OptionDefaultWhat for
type'info''info' | 'success' | 'warning' | 'error' | 'loading'
titlenull
text''
durationundefinedms. null nao fecha sozinho. Padrao depende do tipo
position'bottom-end'top-start|top-center|top-end|bottom-start|bottom-center|bottom-end
closabletrue
actionnull{ text, onClick }
max4toasts simultaneos na mesma posicao