Toast
Aviso passageiro num canto da tela, para o que acabou de acontecer: salvo, enviado, não deu certo. Empilha, pausa quando o ponteiro ou o foco entra e é anunciado por leitor de tela. Para o que continua valendo enquanto a pessoa está na tela, use o aviso.
Exemplos
Cada tipo tem cor, ícone e tempo próprios. O erro fica mais tempo porque a pessoa precisa ler e, muitas vezes, agir; o carregando não fecha sozinho, porque quem o encerra é o fim da operação.
Tipos
info, success, warning, error e loading.
Não fecha sozinho
duration: null deixa na tela até alguém clicar no X.
Tempo próprio
duration em milissegundos, no lugar do padrão do tipo.
Pilha
Até três à mostra, empilhados; o ponteiro ou o foco abre em leque. O quinto fecha o mais antigo.
| Tipo | Fica na tela | Anunciado como |
|---|---|---|
info | 4 s | role="status", região polida |
success | 3,5 s | role="status", região polida |
warning | 6 s | role="status", região polida |
error | 8 s | role="alert", região assertiva |
loading | até o fim da operação | role="status", região polida |
No toast o vermelho é o tipo error. Nas classes do aviso e da etiqueta, o mesmo tom se chama
is-danger.
Como usar
Não há nada para marcar no HTML: o toast é chamado quando algo acontece. O container de cada posição é criado na primeira vez que é usado.
Tucano.toast('Salvo'); // info
Tucano.toast.success('Cadastro salvo');
Tucano.toast.error('Não foi possível salvar', { title: 'Erro 500' });
Tucano.toast({
type: 'success',
title: 'Contrato excluído',
text: 'Ainda dá para voltar atrás.',
action: { text: 'Desfazer', onClick: (t) => restore() },
position: 'bottom-end',
duration: null, // não fecha sozinho
closable: true, // o X
});Os atalhos info, success, warning, error e
loading recebem o texto e, depois, qualquer outra opção. A ação é um botão do sistema: chama o
onClick com o toast e fecha. Tudo devolve a instância, que é o mesmo new Tucano.Toast({ ... })
— ele recebe só as opções, sem alvo.
const t = Tucano.toast('Exportando...');
t.close();
// O evento sai do próprio cartão depois que ele deixa a tela; não borbulha.
t.node.addEventListener('tucano:toast-closed', () => console.log('fechou'));Carregando e promessa
O toast de loading vira o resultado no mesmo cartão, em vez de fechar um e abrir outro:
a pilha não se reorganiza e o olho não perde de vista o aviso que já estava lendo.
// Controle manual: guarde a instância e troque o que precisar.
const t = Tucano.toast.loading('Salvando contrato...');
await save();
t.update({ type: 'success', text: 'Contrato salvo' });
// Ou entregue a promessa e deixe os três estados por conta dela.
Tucano.toast.promise(fetch(url), {
loading: 'Enviando...',
success: (response) => `Enviado (${response.status})`,
error: 'Não deu para enviar',
position: 'top-center', // o resto vale para o toast
});update() troca tipo, título, texto e ação sem recriar o cartão. Mudando de tipo sem
duration, vale o tempo do tipo novo, e o relógio recomeça: sem isso o carregando, que não fecha, viraria
um "salvo" eterno na tela.
Em toast.promise(), success e error aceitam texto ou uma função que recebe o
resultado ou o erro. Sem eles, os textos são "Carregando...", "Pronto" e "Algo deu errado", trocáveis na página inteira por Tucano.setTexts({ toast: { loading, success, error } }). A função devolve a mesma
promessa que recebeu, para não atrapalhar quem já encadeava nela — rejeitada, ela continua rejeitada, e o erro se
trata onde já se trataria.
null quer dizer "não fecha", e não "use o padrão"
O padrão do tempo é resolvido com in, e não com ??. O null do carregando é um valor: com ?? ele cairia nos 4 segundos de padrão, e o toast sumiria no meio da operação.
Posições
Seis cantos e centros, com bottom-end de padrão. Cada posição tem a própria pilha.
No celular, abaixo de 40rem, o toast ocupa a largura da tela e entra pelo eixo vertical, seja qual for a posição: preso a um canto ele ficava com margens diferentes de cada lado, e deslizar na horizontal um cartão tão largo quanto a tela é percurso longo demais para um aviso curto. A borda respeita o indicador de home do iPhone e o recorte da câmera.
max limita os toasts abertos na mesma posição, 4 por padrão; o que passa fecha o mais antigo. Na pilha,
além do terceiro eles somem da vista, porque uma pilha de dez não ajuda ninguém.
Mensagens do Django
O framework de messages vira toast sem uma linha de JavaScript. Ponha o laço uma vez, no
template base: cada <div> vira um toast e sai da página.
{% for m in messages %}
<div data-tuc-toast data-type="{{ m.level_tag }}">{{ m }}</div>
{% endfor %}messages.success(request, "Contrato salvo")
return redirect("contracts")| Atributo | Para quê |
|---|---|
data-tuc-toast | Marca o elemento; o texto dele vira o texto do toast |
data-type | O tipo. debug vira info; info, success, warning e error passam direto |
data-title | Título |
data-text | Texto, no lugar do conteúdo do elemento |
data-duration | Milissegundos; false não fecha sozinho |
data-position | Uma das seis posições |
m.level_tag, e não m.tags, se você usa extra_tags
Do data-type vale só a primeira palavra. O m.tags do Django escreve as extra_tags antes do nível, e aí a primeira palavra deixa de ser o tipo. Sem extra_tags, os dois dão no mesmo.
Pelo cabeçalho do HTMX
Numa resposta de HTMX não há redirect nem template base para o laço de mensagens. O servidor dispara o
toast pelo cabeçalho HX-Trigger, e o script do CDN já está ouvindo.
import json
def save(request, pk):
...
return HttpResponse(headers={
"HX-Trigger": json.dumps({
"tucano:toast": {"type": "success", "text": "Contrato salvo"},
}),
})O detalhe aceita qualquer opção do toast, ou só um texto: {"tucano:toast": "Salvo"}. O HTMX dispara o
evento tucano:toast no elemento da requisição, ele sobe até o <body>, e ali vira toast.
O mesmo evento serve de qualquer outro código:
document.body.dispatchEvent(new CustomEvent('tucano:toast', {
detail: { type: 'success', text: 'Contrato salvo' },
}));Pelo CDN, o listenForEvents() é ligado junto com a inicialização. Importando de tucano pelo
npm nada roda sozinho: chame listenForEvents(), ou importe tucano/auto.
Acessibilidade
O toast não rouba o foco: ele avisa sem tirar a pessoa do que estava fazendo. Quem anuncia é o leitor de tela.
A região é criada antes da mensagem
O aria-live fica no container, e não no toast. Uma região viva precisa existir no DOM antes de o conteúdo chegar; criada junto com a mensagem, o leitor de tela não anuncia. Erro fala numa região assertive e o resto numa polite, e as duas dividem o mesmo palco: posicionadas cada uma por conta, viravam duas pilhas paralelas na tela e o limite contava em dobro.
Quando update() muda um toast para error, ou de error para outro tipo, ele troca de
região, e é isso que faz o leitor de tela anunciar a virada. Na tela nada se desloca, porque a posição vem do palco.
O relógio pausa quando o ponteiro entra no toast ou algo dentro dele recebe foco: ninguém consegue ler algo que some
enquanto tenta clicar no "Desfazer". Pelo mesmo motivo a pilha abre em leque também no foco, e não só no ponteiro. O X
é um botão de verdade, com aria-label="Fechar". Com prefers-reduced-motion, entrar e sair viram
só esmaecer.
API
Gerada do código a cada build — se algo não está aqui, não existe.
[data-tuc-toast]new Tucano.Toast(opcoes) Tucano.toast() Tucano.listenForEvents()data-duration data-position data-text data-title data-typeupdate closetucano:toast-closedOpções
| Opção | Padrão | Para quê |
|---|---|---|
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 |