Uma pilha de avisos curtos e efêmeros, presa a um canto da tela: "relatório enviado", "link copiado", "não foi possível enviar". Cada aviso traz um ícone de tom, o texto, um botão de fechar e uma régua de tempo que encolhe — é ela que torna o desaparecimento previsível em vez de arbitrário. No máximo três ficam de pé ao mesmo tempo; o quarto empurra o mais antigo para fora, e quem sai deixa os de baixo subirem no lugar, sem salto.
É a peça mais modesta desta leva, e isso é julgamento honesto, não desleixo: ela existe para dar retorno a uma ação que não muda de página — envio de formulário, cópia de um dado, download iniciado —, e o problema editorial que resolve é justamente não ter nenhum. Um aviso de erro que ninguém lê é pior que nenhum aviso; por isso a peça pausa sozinha quando alguém está lendo, em vez de confiar que cinco segundos bastam para qualquer frase.
Quando usar
- Como confirmação de uma ação que não navega: formulário enviado, texto copiado, arquivo baixado, preferência salva.
- Quando o retorno pode ser perdido sem drama — se a pessoa piscar e não ver o aviso, a ação ainda aconteceu. Toast é reforço, não o único lugar onde a informação existe.
- Empilhado com outros: a peça já resolve até três avisos simultâneos sem que um cubra o outro.
Quando não usar
- Para erro que bloqueia o próximo passo (campo obrigatório vazio, cartão recusado). Isso é mensagem inline, ao lado do campo — a pessoa não pode precisar lembrar o que um aviso que já sumiu dizia. Ver o gotcha "Sem JavaScript", abaixo, e o
.lead-okdoconversao/cta-formulario-lead. - Para confirmação que exige uma decisão (desfazer, confirmar, escolher entre duas opções). Isso é
overlay/modal-dialogo: o toast fecha sozinho, e uma decisão não pode fechar sozinha. - Para consentimento de cookies. É
overlay/aviso-cookies— parece um toast, mas não pode desaparecer por tempo nem ser um entre quatro: precisa ficar até alguém decidir. - Mais de um aviso por ação. Se uma submissão dispara três toasts em sequência, a pessoa não sabe qual ler primeiro.
Padrão editorial
| Slot | Regra |
|---|---|
| Texto | Uma frase, sem título. Verbo no particípio ou substantivo do que aconteceu: "Relatório consolidado enviado para o seu e-mail", não "Sucesso!". A pessoa já sabe o que tentou fazer; o aviso confirma, não celebra. |
Tom ok |
Ação concluída como esperado. |
Tom info |
Confirmação neutra, sem carga de sucesso ou falha — cópia, atualização. |
Tom atencao |
Ação que não se completou. Diz o que falhou e, quando fizer sentido, o que fazer ("tente novamente em instantes") — nunca só "Erro". |
| Duração | 5s no padrão; avisos de atencao que carregam mais informação (o exemplo de falha no envio usa data-tst-duracao="7000") podem pedir mais. Nunca menos de 4s — é o piso para ler uma frase de 12 palavras sem pressa. |
Nada de retorno prometido. "Relatório enviado" é fato; "Sua carteira vai render mais" não é o tipo de frase que cabe num toast, nem em lugar nenhum desta biblioteca. Os três textos de exemplo no markup.html são ilustrativos, como todo conteúdo desta leva.
Este componente não mostra número, comparação nem captura dado — por isso não há uma seção ## Compliance dedicada (o gatilho para ela, no contrato, é justamente esse). A única exigência que sobra é a voz do texto, coberta acima.
Especificação dos assets
Nenhum. Não há imagem: o único elemento visual além do texto é o ícone de tom, e ele já é <svg> inline no próprio markup.html — não existe arquivo para versionar nem para trocar por projeto.
Como usar
<link rel="stylesheet" href="componentes/overlay/toast-aviso/estilo.css">
<script src="componentes/overlay/toast-aviso/script.js" defer></script>
Cole o bloco da .tst-pilha uma vez, perto do fim do <body> — ela é fixa e vale para a página inteira. Depois, qualquer botão em qualquer lugar da página dispara um aviso com três atributos:
<button type="button" data-tst-dispara data-tst-tom="ok" data-tst-texto="Relatório consolidado enviado para o seu e-mail.">
Enviar relatório
</button>
data-tst-tom aceita info (padrão), ok ou atencao. data-tst-duracao é opcional, em milissegundos (padrão 5000).
Ganchos
| Gancho | Papel |
|---|---|
data-tst-pilha |
A pilha fixa. Raiz única do componente — só a primeira da página é usada (ver gotcha). |
data-tst-template |
O <template> com o esqueleto de um aviso, filho da própria pilha. |
data-tst-dispara |
No botão gatilho, em qualquer parte da página. |
data-tst-texto / data-tst-tom / data-tst-duracao |
Conteúdo, tom e duração do aviso, lidos do gatilho no instante do clique. |
.is-shown |
O aviso visível. Removê-la dispara a mesma transição ao contrário — é o que a saída faz. |
role/aria-live |
Escritos pelo script por tom: status/polite (info, ok) ou alert/assertive (atenção). Não há atributo estático no HTML — ver gotcha de acessibilidade. |
--tst-inset |
Distância da pilha até o canto da tela. |
--tst-gap |
Espaço entre avisos empilhados. |
--tst-largura |
Largura do cartão no desktop; ignorada na faixa mobile. |
--tst-saida |
Duração da transição de saída. Bate com SAIDA_MS no script.js. |
Variações
- Duração por tom — já usada no próprio exemplo:
data-tst-duracao="7000"no botão de falha, contra os 5000 padrão dos outros dois. Não há constante fixa por tom no script; cada gatilho decide. - Canto oposto ou barra superior — troque
right/bottomporleft/topem.tst-pilha. O invólucro e a régua não mudam, só a âncora. - Aviso com ação — um segundo botão ou link dentro de
.tst-linha, antes do botão de fechar ("Desfazer", "Ver relatório"). O pausar-ao-focar já cobre o elemento novo; falta decidir se clicar na ação também fecha o aviso.
Gotchas
-
A pilha é
position:fixede fica fora de qualquer[data-rv], e isso não é estilo — é oblur(0)do reveal do core.[data-rv].intermina emfilter:blur(0), que continua sendo um filtro: um elemento com filtro é bloco de contenção para descendenteposition:fixed(MANUAL §5). Uma pilha nascida dentro de uma seção revelada pordata-rvprenderia o overlay dentro da seção, não da janela — o aviso "flutuante" na verdade ficaria colado a um ponto do meio da página. Por isso.tst-pilhaentra como irmã da seção de exemplo nomarkup.html, nunca como filha de um bloco comdata-rv. -
Não existe token de cor por tom, de propósito. A biblioteca só tem
--accentcomo cor de marca; "verde de sucesso" e "vermelho de alerta" não têm token global, e--tst-ok/--tst-atencaoseriam cor crua escondida atrás de um nome de variável prefixada — o que o §6.6 do contrato não distingue de uma cor solta. A diferença de tom sai por ícone (três<svg>notemplate, um por tom, só umdisplay:blockpor vez) e por papel de acessibilidade. Efeito colateral bom: a peça é legível para quem não distingue vermelho de verde, sem precisar de modo especial nenhum. Candidato a discussão de arquitetura, relatado à parte — ver relatório final do orquestrador. -
Cada aviso é a própria região viva; não existe um contêiner de
aria-livefixo na pilha. Tominfo/oknascerole="status"(implicapolite); tomatencaonascerole="alert"(implicaassertive). Duas regiões permanentes — umapolite, umaassertive— se atropelariam no instante em que chegasse um aviso de cada tipo ao mesmo tempo, e leitor de tela nenhum garante ordem entre duas regiões que mudam junto. Como o elemento inteiro (já com orolecerto) entra pronto no DOM a partir do<template>, o leitor de tela recebe a própria inserção como o anúncio. -
A saída colapsa por
grid-template-rows: 0fr -> 1fr, não por altura medida em pixel. É o mesmo problema que motivou o--perf-palco-medidodoconversao/diagnostico-perfil, com a solução oposta: lá o conteúdo muda de tamanho de formas imprevisíveis e só um script medindo resolve; aqui a caixa não muda de forma, só aparece e desaparece, e o truque do grid resolve sem o script tocar emgetBoundingClientRect..tst-itemé o invólucro (a linha do grid que anima);.tst-caixaé o conteúdo, commin-height:0eoverflow:hidden— as duas propriedades sem as quais o item de grid nunca encolhe abaixo do próprio conteúdo. -
.tst-iteme.tst-caixasão nós diferentes pelo mesmo motivo do §5 do contrato sobretransform. Aqui não hátransformde repouso disputando especificidade com o core, mas a lição é irmã: o nó que leva a transição de layout (grid-template-rows) não pode ser o mesmo que precisa de conteúdo com altura livre — juntar os dois exigiria animarmax-heightcom um valor cravado, que é exatamente o que o truque do grid evita. -
A régua de tempo não tem
transitionno CSS. Ela é redesenhada portransform:scaleX()a cada quadro dorequestAnimationFrame, com o valor calculado pordt(teto de 50ms). Uma transição CSS por cima brigaria com um valor que já muda a ~60× por segundo, e a régua perderia precisão — o sintoma seria uma barra que sempre "persegue" o valor real com um pequeno atraso. É também por isso que ela continua se movendo em movimento reduzido: nunca teve animação declarativa para desligar, e ela é informação (quanto tempo falta), não enfeite — a única peça do componente com essa exceção explícita. Medido: comprefers-reduced-motion:reduce,transition-durationdo invólucro cai para1e-5se ainda assim a régua de um aviso de 900ms foi descaleX(.9497)ascaleX(.5516)em 350ms — a mesma curva de queda de quando o movimento não está reduzido — e o aviso some sozinho no tempo certo. -
O quarto aviso empurra o mais antigo pelo MESMO caminho de qualquer fechamento, nunca por um atalho.
montar()chama o mesmofechar()que o botão e o fim do tempo chamam; é por isso que o mais antigo sai com a mesma transição, e os de baixo sobem sem salto, em vez de a pilha simplesmente cortar para três. Medido: 4 cliques no mesmo botão, esperando a saída terminar, deixam exatamente 3.tst-itemno DOM, todos com.is-shown. -
Só a primeira
.tst-pilhada página é usada — de propósito, ao contrário do resto da biblioteca. Oscript.jsfazdocument.querySelector(singular), nãoquerySelectorAll. Os outros componentes autorais podem repetir na página sem colidir; um toast não ganha nada em repetir, porque duas pilhas ativas disputariam o mesmo canto ou exigiriam uma decisão de layout (qual pilha recebe qual aviso) que é da página, não do componente. -
A escuta de clique é delegada no
document, não na raiz do componente — a mesma exceção que modal e drawer têm ao §7.6 do contrato. A regra "parta da raiz" existe para não colidir com uma cópia irmã; um toast não tem esse risco (só uma pilha conta, ver acima) e tem o problema oposto: o gatilho é, por definição, qualquer parte da página. Travar a escuta empilha.contains(gatilho)exigiria que cada botão de disparo morasse dentro da pilha fixa, o que descreve nenhum uso real. -
Pausar ao focar usa
focusin/focusoutcom checagem derelatedTarget, não sóblur/focus. Um aviso com uma segunda ação (ver variação "aviso com ação") tem dois elementos tabuláveis; sem checar se o novo foco ainda está dentro do mesmo.tst-item, tabular do texto para o botão de fechar retomaria a contagem por um quadro antes de pausar de novo — pequeno, mas seria a régua "piscando" a cada Tab. -
O
<button class="btn btn-ghost">dos gatilhos de exemplo já foi um problema, e a correção está no core. A primitiva.btnnão zeravabackgroundnemappearance, e comcolor-scheme:only light(tokens.css) o<button>pintava o cromo nativo do controle, cinza claro, sob o texto claro do tema escuro — medido emrgb(240,240,240)contrargb(242,240,236), legenda invisível. Nunca apareceu em<a class="btn">porque âncora não temappearancenativo, e é por isso que passou tanto tempo despercebido. Corrigido emcore/base.cssem 2026-09-06, na própria regra.btn; o remendo local que este componente carregava foi removido junto. Se algum dia a legenda de um botão sumir num tema escuro, é aqui que se olha. -
O botão de fechar é um
<button>de verdade, e oEscfecha só o aviso com foco dentro — nunca a pilha inteira. Fechar três avisos de uma vez porque a pessoa quis fechar um é agressivo demais para uma peça deste tamanho; e sem foco em nenhum aviso,Escnão faz nada (não há "o" aviso ativo numa pilha — pode haver três). Medido: o botão é<button>,disabled:false,tabIndex:0— nada no componente tira ele da ordem natural de tabulação nem simula clique com outro elemento. -
A pilha inteira é
pointer-events:none; só cada.tst-itemdevolveauto. Sem isso, a caixa invisível da pilha (que existe só para posicionar — ela não tem fundo) bloquearia clique na página por baixo mesmo nos vãos entre avisos e abaixo do último. Medido com dois avisos empilhados: um clique no meio de um deles acha o próprio toast (elementFromPointretorna um nó dentro do.tst-item); um clique no vão entre os dois atravessa e acha o<html>por trás — a página continua clicável ao redor da pilha. -
No celular a pilha vira faixa, e o
--tst-inseté a mesma variável nos dois modos. Sóleft/right/widthmudam na media query; o respiro até a borda é o mesmo clamp em qualquer largura, o que mantém o alinhamento visual previsível. Medido a 390px: pilha de 14px a 376px (362px de largura útil), sem estourar para nenhum lado, e sem overflow horizontal na página com um aviso aberto.
Histórico
| Projeto | Onde | Data | Diferenças |
|---|---|---|---|
| autoral | construído do zero na biblioteca | 2026-09-06 | — |
Origem. Não há referência única e não há uma linha copiada. O padrão — avisos curtos empilhados num canto, um por ação concluída, desaparecendo sozinhos com uma régua de tempo — é praticamente universal em produto digital: notificação de sistema operacional (macOS, Windows), snackbar do Material Design, e a leva recente de bibliotecas de toast de dashboard SaaS (Stripe, Linear, Vercel). O que a biblioteca tirou dessas famílias foi a forma da tela — canto, empilhamento, régua —, nunca o código.
Decisões da tradução para o contrato:
- A régua de tempo conta por
dtcom teto de 50ms numrequestAnimationFrameque dorme quando nenhum aviso está contando, em vez de uma animação CSS declarativa — é o que permite pausar e retomar de onde parou em vez de reiniciar, e o que respeita o §3/§7 do contrato sobre tempo, não quadro. - O colapso de altura na saída usa
grid-template-rowsem vez de o script medirscrollHeight: zero JavaScript de layout, e a transição roda mesmo se o conteúdo do aviso mudar de uma linha para duas (texto mais longo num projeto futuro). - A diferença de tom é ícone e papel de acessibilidade, nunca cor — a biblioteca não tem token semântico de sucesso/alerta, e inventar um seria cor crua disfarçada de variável prefixada.
- Cada aviso é a própria região
aria-live, em vez de um contêiner permanente único: um contêiner só escolheria entrepoliteeassertiveuma vez para a sessão inteira, e erraria toda vez que chegasse um aviso do outro tipo. - Disparo por
data-tst-disparadelegado nodocument: é a mesma exceção de modal e drawer ao §7.6 do contrato, porque o gatilho de um toast é, por definição, qualquer lugar da página — nunca um descendente da própria pilha.
Sem JavaScript. Sem script, o <template> nunca vira DOM e os três botões de exemplo não fazem nada — o que é honesto: um toast é o retorno de uma ação que já depende de JavaScript (enviar formulário, copiar para a área de transferência). Ele não substitui a mensagem inline de erro de um formulário; complementa. Quem precisa de retorno que sobrevive a JavaScript desligado usa mensagem inline no próprio campo, como o .lead-ok do conversao/cta-formulario-lead.