← Biblioteca componentes/overlay/toast-aviso/ficha.md CódigoTela cheia
overlay/toast-aviso

Toast de aviso

🟢 estavel complexidade média prefixo tst- toast aviso notificacao snackbar pilha feedback aria-live acessibilidade microinteracao sem-lib overlay

Incluir · as linhas que se colam num site novo, depois do core (CLAUDE.md §10)

<link rel="stylesheet" href="componentes/overlay/toast-aviso/estilo.css">
<script src="componentes/overlay/toast-aviso/script.js"></script>

markup.html · 113 linhas · 6,8 KB

<!-- ==========================================================================
     TOAST DE AVISO — pilha de avisos efêmeros, empilhados num canto
     Prefixo: tst-
     Requer: core (tokens, base, motion.css, motion.js) + estilo.css + script.js
     Usa a primitiva .glass do core.

     A IDEIA. Retorno curto para uma ação que não muda de página: "relatório
     enviado", "link copiado", "falha no envio". A pilha vive presa a um canto
     da tela (inferior direito no desktop; faixa inferior no celular), mostra
     no máximo três avisos por vez — o quarto empurra o mais antigo para fora
     — e cada um carrega uma RÉGUA DE TEMPO que encolhe: é o que torna o
     desaparecimento previsível em vez de arbitrário.

     DISPARO É DECLARATIVO, como o §7 do contrato pede (nenhum escopo
     global): qualquer botão da página com data-tst-dispara, data-tst-texto
     e data-tst-tom monta um aviso a partir do <template> logo abaixo. Os três
     botões no fim deste arquivo são o EXEMPLO, um por tom — troque por um
     gatilho real (fim de envio de formulário, botão de copiar, callback de
     erro de rede).

     A PILHA É position:fixed E FICA FORA DE QUALQUER [data-rv]. blur(0) — o
     estado final do reveal do core — ainda é filtro, e filtro é bloco de
     contenção para descendente fixed (contrato §5 / MANUAL §5): um data-rv
     em volta da pilha prenderia o overlay dentro da seção que o revelou, em
     vez da janela. Por isso .tst-pilha entra direto como irmã da seção de
     exemplo, nunca dentro dela.

     TOM SEM COR CRUA. A biblioteca só tem UM token de cor de marca
     (--accent) — não existe "verde de sucesso" nem "vermelho de alerta" no
     vocabulário global, e inventar --tst-ok/--tst-atencao seria cor crua
     escondida atrás de um nome de variável. A diferença entre info/ok/atenção
     é por ÍCONE (i, check, triângulo) e por PAPEL de acessibilidade — nunca
     por matiz. Ver ficha.md, seção "O que o contrato não deixou".

     ACESSIBILIDADE. Cada aviso É a própria região viva: tom info/ok nasce
     role="status" (implica aria-live polite); tom atenção nasce role="alert"
     (implica aria-live assertive). Não existe UM contêiner externo com
     aria-live fixo — dois contêineres permanentes, um polite e um assertive,
     se atropelariam no instante em que um aviso de cada tipo chegasse junto.
     Como o elemento inteiro (já com o role certo) entra no DOM pronto, o
     leitor de tela anuncia a própria inserção como novidade.

     SEM JAVASCRIPT NÃO HÁ TOAST — e está certo: ele é o retorno de uma ação
     que já depende de JavaScript (enviar formulário, copiar). Sem script, o
     <template> nunca vira DOM e os botões de exemplo não fazem nada; o lugar
     do retorno sem JS é MENSAGEM INLINE no próprio formulário, como o
     .lead-ok do conversao/cta-formulario-lead. Ver ficha.md, seção
     "Sem JavaScript".
     ========================================================================== -->

<section class="sec-dark sec-pad">
  <div class="wrap">
    <span class="eyebrow" data-rv>Retorno de ação</span>
    <h2 class="sec-title ttl">Um aviso curto para uma ação que não muda de <em>página</em></h2>
    <p class="sec-lead" data-rv data-d="1">Três gatilhos de exemplo, um por tom. Clique em mais de um para ver a pilha empilhar — e um quarto para ver o mais antigo sair.</p>

    <div class="tst-demo" data-rv data-d="2">
      <button type="button" class="btn btn-ghost" data-tst-dispara data-tst-tom="ok" data-tst-texto="Relatório consolidado enviado para o seu e-mail.">
        Enviar relatório
      </button>
      <button type="button" class="btn btn-ghost" data-tst-dispara data-tst-tom="info" data-tst-texto="Dado copiado para a área de transferência.">
        Copiar dado
      </button>
      <button type="button" class="btn btn-ghost" data-tst-dispara data-tst-tom="atencao" data-tst-texto="Não foi possível enviar o formulário. Tente novamente em instantes." data-tst-duracao="7000">
        Simular falha no envio
      </button>
    </div>
  </div>
</section>

<!-- A pilha e o <template> vivem FORA da seção de exemplo, no fluxo direto
     do documento: nunca dentro de um [data-rv]. Num site consumidor, este
     bloco cola uma vez só, perto do fim do <body>. -->
<div class="tst-pilha" data-tst-pilha aria-label="Avisos">

  <template data-tst-template>
    <!-- .tst-item é o INVÓLULO: só ele leva a transição de entrada/saída
         (grid-template-rows 0fr -> 1fr, opacity, blur). .tst-caixa é o
         conteúdo de verdade, com overflow:hidden e min-height:0 — as duas
         propriedades que fazem o truque do grid-rows colapsar a altura sem
         o script medir nada em pixel. É o mesmo padrão do §5 do contrato
         para transform: o que anima e o que se move não moram no mesmo nó. -->
    <div class="tst-item" data-tst-item>
      <div class="tst-caixa glass">
        <div class="tst-linha">
          <!-- Três ícones, um por tom; o CSS mostra só o que casa com
               data-tst-tom (escrito pelo script no próprio .tst-item). A cor
               de cada um é token (currentColor herdado), nunca um SVG
               colorido à mão — é o que permite reler o ícone certo em
               qualquer tema. -->
          <svg class="tst-icone tst-icone-ok" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
            <circle cx="10" cy="10" r="8.2"/><path d="M6.2 10.3l2.5 2.5 5-5.4"/>
          </svg>
          <svg class="tst-icone tst-icone-info" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
            <circle cx="10" cy="10" r="8.2"/><path d="M10 9.4v4.4"/><circle cx="10" cy="6.3" r="1" fill="currentColor" stroke="none"/>
          </svg>
          <svg class="tst-icone tst-icone-atencao" width="20" height="20" viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
            <path d="M10 2.6l8 14.2H2z" stroke-linejoin="round"/><path d="M10 8.2v3.8"/><circle cx="10" cy="14.4" r="1" fill="currentColor" stroke="none"/>
          </svg>

          <p class="tst-texto" data-tst-texto></p>

          <button type="button" class="tst-fechar" data-tst-fechar aria-label="Fechar aviso">
            <svg width="10" height="10" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" aria-hidden="true"><path d="M1 1l10 10M11 1L1 11"/></svg>
          </button>
        </div>

        <div class="tst-regua" aria-hidden="true"><span class="tst-regua-barra" data-tst-barra></span></div>
      </div>
    </div>
  </template>

</div>

estilo.css · 177 linhas · 6,6 KB

/* ==========================================================================
   TOAST DE AVISO — prefixo tst-
   Pilha fixa num canto da tela, no máximo três avisos por vez, cada um com
   ícone de tom, texto, botão de fechar e uma régua de tempo que encolhe.

   Tokens usados:
     --accent           ícone do tom "ok" e a régua em progresso
     --fg               texto do aviso, ícone do tom "atenção"
     --fg-2             ícone do tom "info", botão de fechar em repouso
     --rule             trilho em repouso da régua de tempo
     --ease             curva única do projeto
     --sans             face do texto do aviso
     --radius-media     canto do cartão (0 em projeto flat, e está certo)
     --radius-pill      régua e botão de fechar, os dois circulares/pílula
   Primitiva do core em uso: .glass (superfície do cartão).

   POR QUE NÃO HÁ TOKEN DE COR POR TOM: só existe --accent como cor de marca
   na biblioteca. "Verde de sucesso" e "vermelho de alerta" não têm token —
   e --tst-ok/--tst-atencao seriam cor crua disfarçada de variável, o que o
   §6.6 do contrato proíbe do mesmo jeito. A diferença de tom sai por ÍCONE
   (três <svg> no markup, um por tom, só um visível por vez) e por PAPEL de
   acessibilidade (role="status" vs "alert", escrito pelo script) — nunca por
   matiz. Ver ficha.md, seção "O que o contrato não deixou".
   ========================================================================== */

/* Fileira dos três gatilhos de exemplo (um por tom). Não é a peça — é o
   convite a clicar nela; um site consumidor troca por botões reais e pode
   apagar esta regra junto com o markup de exemplo. */
.tst-demo{
  margin-top:clamp(26px,4vh,40px);
  display:flex;
  flex-wrap:wrap;
  gap:14px;
}
.tst-pilha{
  --tst-inset:clamp(14px,2.4vw,26px);
  --tst-gap:12px;
  --tst-largura:360px;
  /* Bate com a duração da transição de .tst-item, logo abaixo. O script lê
     este número (SAIDA_MS) para saber quando remover o nó da pilha — mudar
     um pede mudar o outro. */
  --tst-saida:.36s;

  position:fixed;
  right:var(--tst-inset);
  bottom:var(--tst-inset);
  z-index:900;
  display:flex;
  flex-direction:column;
  /* Novo aviso entra no fim da lista (mais perto do canto); o mais antigo
     fica no topo da pilha e é o primeiro a sair — nem por acaso é também
     o primeiro a ser empurrado quando chega o quarto. */
  gap:var(--tst-gap);
  width:var(--tst-largura);
  max-width:calc(100vw - (var(--tst-inset) * 2));
  /* A caixa da pilha não tem fundo próprio: só existe para posicionar. Sem
     pointer-events:none aqui, a folga entre avisos (gap) e a sobra abaixo do
     último bloquearia clique na página por baixo, mesmo estando vazia. */
  pointer-events:none;
}

/* ==========================================================================
   O INVÓLUCRO — entrada, saída e o colapso de altura
   Truque grid-template-rows: 0fr -> 1fr. Funciona sem o script medir um
   pixel sequer, e é o que faz os avisos de baixo SUBIREM ao vivo quando um
   de cima sai, em vez de a pilha simplesmente encolher de repente.
   ========================================================================== */
.tst-item{
  display:grid;
  grid-template-rows:0fr;
  opacity:0;
  filter:blur(14px);
  pointer-events:auto;
  transition:grid-template-rows var(--tst-saida) var(--ease),
             opacity .3s var(--ease),
             filter .38s var(--ease);
}
.tst-item.is-shown{
  grid-template-rows:1fr;
  opacity:1;
  filter:blur(0);
}

/* Movimento reduzido desliga só o que anda SOZINHO — a entrada e a saída do
   invólucro. A régua de tempo (mais abaixo) nem precisa de regra aqui: ela
   nunca teve transition própria, então já não anima em nenhum dos dois
   casos. filter:none em vez de deixar herdado: blur(14px) parado no meio
   travaria o aviso meio-transparente para sempre em quem pediu menos
   movimento. Colada à regra que desliga, como manda o §6.6 do contrato. */
@media(prefers-reduced-motion:reduce){
  .tst-item{transition-duration:.01ms;filter:none}
}

.tst-caixa{
  /* As duas linhas que fazem o grid-rows valer: sem min-height:0 o item de
     grid nunca encolhe abaixo do conteúdo, e sem overflow:hidden o texto
     vaza da caixa de altura zero antes de ela crescer. */
  min-height:0;
  overflow:hidden;
  border-radius:var(--radius-media);
  padding:14px 14px 12px;
}

.tst-linha{
  display:flex;
  align-items:flex-start;
  gap:11px;
}

.tst-icone{
  display:none;
  flex:0 0 20px;
  margin-top:1px;
}
/* Só o ícone do tom corrente aparece — data-tst-tom é escrito pelo script no
   próprio .tst-item, no instante em que o aviso é montado. */
.tst-item[data-tst-tom="ok"] .tst-icone-ok{display:block;color:var(--accent)}
.tst-item[data-tst-tom="info"] .tst-icone-info{display:block;color:var(--fg-2)}
.tst-item[data-tst-tom="atencao"] .tst-icone-atencao{display:block;color:var(--fg)}

.tst-texto{
  flex:1 1 auto;
  font-family:var(--sans);
  font-size:14px;
  line-height:1.5;
  color:var(--fg);
}

.tst-fechar{
  flex:0 0 auto;
  width:26px;height:26px;padding:0;margin:-2px -2px 0 0;
  display:grid;place-items:center;
  border-radius:var(--radius-pill);
  background:none;border:0;color:var(--fg-2);
  transition:color .25s var(--ease);
}
.tst-fechar:hover,
.tst-fechar:focus-visible{color:var(--accent)}

/* ==========================================================================
   RÉGUA DE TEMPO
   O script escreve transform:scaleX(fração) A CADA QUADRO, por dt (nunca
   por quadro cru — teto de 50ms, contrato §3/§7). Por isso a barra NÃO leva
   transition própria: uma transição brigaria com um valor que já muda a
   cada ~16ms, e a régua perderia a leitura fina do tempo restante — inclusive
   com movimento reduzido, onde ela é a ÚNICA coisa que ainda se move, porque
   é informação (quanto falta), não enfeite.
   ========================================================================== */
.tst-regua{
  position:relative;
  height:2px;
  margin-top:11px;
  background:var(--rule);
  border-radius:var(--radius-pill);
  overflow:hidden;
}
.tst-regua-barra{
  position:absolute;inset:0;
  transform:scaleX(1);
  transform-origin:left;
  background:var(--accent);
}

/* ==========================================================================
   MOBILE — faixa inferior, não canto
   Sob o dedo um cartão de 360px ancorado no canto sobra fora da mira; a
   pilha vira uma faixa que ocupa a largura útil, ainda encostada embaixo.
   ========================================================================== */
@media(max-width:560px){
  .tst-demo .btn{width:100%}
  .tst-pilha{
    left:var(--tst-inset);
    right:var(--tst-inset);
    width:auto;
    max-width:none;
  }
}

script.js · 214 linhas · 9,0 KB

/* ==========================================================================
   TOAST DE AVISO — prefixo tst-

   O QUE ESTE SCRIPT FAZ, EM ORDEM:
     1. ouve clique em QUALQUER [data-tst-dispara] da página (delegado no
        document — ver nota "BUSCA GLOBAL", abaixo) e monta um aviso a
        partir do <template> que mora dentro da pilha;
     2. escreve data-tst-tom, role/aria-live e o texto no clone, mostra com
        blur indo a zero, e empilha (o quarto empurra o mais antigo);
     3. um laço de requestAnimationFrame por PILHA conta o tempo de cada
        aviso por dt (teto de 50ms — contrato §3/§7) e escreve a régua;
     4. pausa a contagem de um aviso ao passar o ponteiro ou ao receber
        foco, retomando de onde parou — nunca do início;
     5. fecha por botão, por tempo esgotado, por Esc (com foco dentro do
        aviso) ou por estouro da fila.

   BUSCA GLOBAL, E POR QUE ELA NÃO FERE O §7.6. A regra é "parta da raiz do
   próprio componente", para não colidir com uma cópia irmã na mesma página.
   Um toast é o caso em que isso não se aplica: o gatilho é, por definição,
   qualquer parte da página — um formulário no topo, um botão no rodapé — e
   travar a escuta em pilha.contains(gatilho) exigiria que cada disparo
   morasse dentro da pilha, o que não é o caso de nenhum uso real. A MESMA
   exceção vale para modal e drawer: o alvo do gancho é a página, a peça é
   que é única.

   POR ISSO SÓ A PRIMEIRA PILHA CONTA. Ao contrário dos outros componentes
   da casa, esta peça não ganha nada em repetir na página: duas pilhas
   disputariam o mesmo canto, ou precisariam de dois cantos combinados por
   fora — decisão de layout de página, não deste componente. document.
   querySelector (singular, não All) é deliberado.

   NENHUM DADO É GRAVADO. O texto de cada aviso vive só no DOM enquanto ele
   está de pé; não há localStorage, cookie nem envio — inclusive porque o
   §4 do brief proíbe localStorage de dado de pessoa, e o texto de um toast
   pode muito bem conter um.
   ========================================================================== */

(function () {

  var pilha = document.querySelector('[data-tst-pilha]');
  if (!pilha) return;                                  // guard silencioso

  var template = pilha.querySelector('[data-tst-template]');
  if (!template) return;

  var DURACAO_PADRAO = 5000;    /* ms de vida sem data-tst-duracao no gatilho */
  var MAX_VISIVEL     = 3;
  /* Bate com --tst-saida no estilo.css. Mudar um pede mudar o outro. */
  var SAIDA_MS = 360;

  function reduzido() {
    if (window.LIB && typeof window.LIB.reduce === 'boolean') return window.LIB.reduce;
    return matchMedia('(prefers-reduced-motion: reduce)').matches &&
           location.search.indexOf('motion=on') < 0;
  }

  var fila = [];        /* avisos vivos, mais antigo primeiro */
  var laco = 0;
  var ultimoTS = 0;

  /* -------------------------------------------------------- o laço de tempo
     Um requestAnimationFrame só para a pilha inteira — não um por aviso — e
     ele DORME (laco = 0) assim que não sobra nenhum aviso contando, em vez
     de ficar vivo pela sessão inteira. dt tem teto de 50ms: sem ele, voltar
     de uma aba em segundo plano entregaria um dt de vários segundos e
     fecharia três avisos de uma vez, em silêncio — exatamente o bug que o
     brief pede para não deixar acontecer. */
  function quadro(ts) {
    var dt = ultimoTS ? Math.min(ts - ultimoTS, 50) : 16;
    ultimoTS = ts;
    var k, aviso, algumContando = false;

    for (k = 0; k < fila.length; k++) {
      aviso = fila[k];
      if (aviso.saindo || aviso.pausado) continue;
      algumContando = true;
      aviso.restante -= dt;
      var fracao = aviso.restante > 0 ? (aviso.restante / aviso.duracao) : 0;
      aviso.barra.style.transform = 'scaleX(' + fracao + ')';
      if (aviso.restante <= 0) fechar(aviso);
    }

    laco = algumContando ? requestAnimationFrame(quadro) : 0;
  }

  function pedirLaco() {
    if (laco) return;
    ultimoTS = 0;                  /* próximo quadro mede dt=16, nunca o hiato parado */
    laco = requestAnimationFrame(quadro);
  }

  /* ------------------------------------------------------------- fechar/sair */
  function remover(aviso) {
    var i = fila.indexOf(aviso);
    if (i > -1) fila.splice(i, 1);
    if (aviso.el.parentNode) aviso.el.parentNode.removeChild(aviso.el);
  }

  function fechar(aviso) {
    if (aviso.saindo) return;
    aviso.saindo = true;
    aviso.el.classList.remove('is-shown');

    if (reduzido()) { remover(aviso); return; }

    /* transitionend do grid-template-rows é o fim de verdade; o setTimeout
       é rede de segurança — uma transição cancelada no meio (ex.: aba que
       perde o foco bem nessa hora) nunca dispara o evento, e sem a rede o
       aviso ficaria para sempre invisível, mas vivo no DOM. */
    var feito = false;
    function aoFim(e) {
      if (e && (e.target !== aviso.el || e.propertyName !== 'grid-template-rows')) return;
      if (feito) return;
      feito = true;
      aviso.el.removeEventListener('transitionend', aoFim);
      remover(aviso);
    }
    aviso.el.addEventListener('transitionend', aoFim);
    setTimeout(aoFim, SAIDA_MS + 200);
  }

  function pausar(aviso)  { aviso.pausado = true; }
  function retomar(aviso) { if (!aviso.saindo) { aviso.pausado = false; pedirLaco(); } }

  /* ---------------------------------------------------------------- montar */
  function montar(tom, texto, duracaoAttr) {
    if (!texto) return;                  /* gatilho sem data-tst-texto não monta nada */

    var no  = template.content.cloneNode(true);
    var el  = no.querySelector('[data-tst-item]');
    var txt = el.querySelector('[data-tst-texto]');
    var fch = el.querySelector('[data-tst-fechar]');
    var bar = el.querySelector('[data-tst-barra]');

    txt.textContent = texto;
    el.setAttribute('data-tst-tom', tom);

    /* Papel de acessibilidade: CADA AVISO é a própria região viva. Não há um
       contêiner externo com aria-live fixo na pilha — dois contêineres
       permanentes (um polite, um assertive) se atropelariam no instante em
       que um aviso de cada tipo chegasse ao mesmo tempo. Como o elemento
       inteiro entra pronto no DOM (via <template>), o leitor de tela recebe
       a própria inserção como o anúncio — não precisa de um live region que
       já existisse de antemão. */
    if (tom === 'atencao') {
      el.setAttribute('role', 'alert');
      el.setAttribute('aria-live', 'assertive');
    } else {
      el.setAttribute('role', 'status');
      el.setAttribute('aria-live', 'polite');
    }

    var duracao = parseFloat(duracaoAttr) || DURACAO_PADRAO;
    var aviso = {
      el: el, barra: bar,
      duracao: duracao, restante: duracao,
      pausado: false, saindo: false
    };

    /* Pausa ao passar o ponteiro OU ao receber foco (Tab até o botão de
       fechar), e só retoma quando os dois soltam. Sem isso, um aviso de
       leitura mais longa pode fechar enquanto ainda está sendo lido — bug
       que o brief chama pelo nome. */
    el.addEventListener('pointerenter', function () { pausar(aviso); });
    el.addEventListener('pointerleave', function () { retomar(aviso); });
    el.addEventListener('focusin',  function () { pausar(aviso); });
    el.addEventListener('focusout', function (e) {
      if (el.contains(e.relatedTarget)) return;   /* foco só mudou de botão, dentro do mesmo aviso */
      retomar(aviso);
    });

    fch.addEventListener('click', function () { fechar(aviso); });

    pilha.appendChild(el);
    fila.push(aviso);

    /* Reflow forçado entre inserir (estado fechado, grid-template-rows:0fr)
       e ligar .is-shown: sem ele o navegador junta as duas mudanças no
       mesmo recálculo e a transição de entrada não roda — mesma pegadinha
       documentada no conversao/diagnostico-perfil. */
    void el.offsetWidth;
    el.classList.add('is-shown');

    /* O QUARTO EMPURRA O MAIS ANTIGO. Fecha fila[0] (não o item recém-criado)
       assim que a fila passa de três — o mesmo caminho de saída de qualquer
       outro fechamento, então a pilha sobe do mesmo jeito, sem salto. */
    if (fila.length > MAX_VISIVEL) fechar(fila[0]);

    pedirLaco();
  }

  /* --------------------------------------------------------------- eventos */
  document.addEventListener('click', function (e) {
    var gatilho = e.target.closest ? e.target.closest('[data-tst-dispara]') : null;
    if (!gatilho) return;
    montar(
      gatilho.getAttribute('data-tst-tom') || 'info',
      gatilho.getAttribute('data-tst-texto'),
      gatilho.getAttribute('data-tst-duracao')
    );
  });

  /* Esc fecha o aviso que TEM o foco dentro — nunca a pilha inteira: apagar
     três avisos de uma vez porque a pessoa quis fechar um só é agressivo
     demais para uma peça desse tamanho. */
  document.addEventListener('keydown', function (e) {
    if (e.key !== 'Escape') return;
    var k;
    for (k = 0; k < fila.length; k++) {
      if (fila[k].el.contains(document.activeElement)) { fechar(fila[k]); return; }
    }
  });

})();

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

Quando não usar


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


Gotchas


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:

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.