Como criar um componente de aviso

Uma visão geral básica de como criar um componente de toast adaptável e acessível.

Nesta postagem, quero compartilhar ideias sobre como criar um componente de toast. Teste a demonstração.

Demonstração

Se preferir vídeo, confira uma versão desta postagem no YouTube:

Visão geral

Os toasts são mensagens curtas não interativas, passivas e assíncronas para os usuários. Geralmente, eles são usados como um padrão de feedback da interface para informar ao usuário sobre os resultados de uma ação.

Interações

Os toasts são diferentes de notificações, alertas e solicitações porque não são interativos, não devem ser dispensados nem persistir. As notificações são para informações mais importantes, mensagens síncronas que exigem interação ou mensagens no nível do sistema (em vez de no nível da página). Os toasts são mais passivos do que outras estratégias de aviso.

Marcação

O elemento <output> é uma boa opção para o toast porque é anunciado aos leitores de tela. O HTML correto fornece uma base segura para aprimorarmos com JavaScript e CSS, e haverá muito JavaScript.

Um brinde

<output class="gui-toast">Item added to cart</output>

Ele pode ser mais inclusivo ao adicionar role="status". Isso fornece um fallback se o navegador não atribuir aos elementos <output> a função implícita de acordo com a especificação.

<output role="status" class="gui-toast">Item added to cart</output>

Um contêiner de toast

Mais de um aviso pode ser mostrado por vez. Para orquestrar várias notificações, um contêiner é usado. Esse contêiner também processa a posição dos toasts na tela.

<section class="gui-toast-group">
  <output role="status">Wizard Rose added to cart</output>
  <output role="status">Self Watering Pot added to cart</output>
</section>

Layouts

Optei por fixar os toasts na inset-block-end da janela de visualização. Se mais toasts forem adicionados, eles vão se acumular a partir dessa borda da tela.

Contêiner da GUI

O contêiner de toasts faz todo o trabalho de layout para apresentar toasts. Ele é fixed à janela de visualização e usa a propriedade lógica inset para especificar quais bordas fixar, além de um pouco de padding da mesma borda block-end.

.gui-toast-group {
  position: fixed;
  z-index: 1;
  inset-block-end: 0;
  inset-inline: 0;
  padding-block-end: 5vh;
}

Captura de tela com o tamanho da caixa e o padding do DevTools sobrepostos em um elemento .gui-toast-container.

Além de se posicionar na janela de visualização, o contêiner de toast é um contêiner de grade que pode alinhar e distribuir toasts. Os itens são centralizados como um grupo com justify-content e individualmente com justify-items. Adicione um pouco de gap para que as torradas não se toquem.

.gui-toast-group {
  display: grid;
  justify-items: center;
  justify-content: center;
  gap: 1vh;
}

Captura de tela com a sobreposição da grade CSS no grupo de toast, desta vez destacando o espaço e as lacunas entre os elementos filhos do toast.

Toast da GUI

Um toast individual tem alguns padding, alguns cantos mais suaves com border-radius e uma função min() para ajudar no dimensionamento para dispositivos móveis e computadores. O tamanho responsivo no CSS a seguir impede que os toasts fiquem mais largos que 90% da janela de visualização ou 25ch.

.gui-toast {
  max-inline-size: min(25ch, 90vw);
  padding-block: .5ch;
  padding-inline: 1ch;
  border-radius: 3px;
  font-size: 1rem;
}

Captura de tela de um único elemento .gui-toast, com o padding e o raio da borda mostrados.

Estilos

Com o layout e o posicionamento definidos, adicione CSS que ajude na adaptação às configurações e interações do usuário.

Contêiner de toast

Os toasts não são interativos. Tocar ou deslizar neles não faz nada, mas eles consomem eventos de ponteiro. Evite que os toasts roubem cliques com o seguinte CSS.

.gui-toast-group {
  pointer-events: none;
}

Toast da GUI

Dê aos toasts um tema adaptável claro ou escuro com propriedades personalizadas, HSL e uma consulta de mídia de preferência.

.gui-toast {
  --_bg-lightness: 90%;

  color: black;
  background: hsl(0 0% var(--_bg-lightness) / 90%);
}

@media (prefers-color-scheme: dark) {
  .gui-toast {
    color: white;
    --_bg-lightness: 20%;
  }
}

Animação

Um novo toast precisa aparecer com uma animação ao entrar na tela. Para acomodar o movimento reduzido, defina os valores translate como 0 por padrão, mas atualize o valor de movimento para um comprimento em uma consulta de mídia de preferência de movimento . Todos recebem alguma animação, mas apenas alguns usuários têm o deslocamento do toast por uma distância.

Estes são os frames-chave usados para a animação do toast. O CSS vai controlar a entrada, a espera e a saída do toast, tudo em uma animação.

@keyframes fade-in {
  from { opacity: 0 }
}

@keyframes fade-out {
  to { opacity: 0 }
}

@keyframes slide-in {
  from { transform: translateY(var(--_travel-distance, 10px)) }
}

Em seguida, o elemento de toast configura as variáveis e organiza os frames-chave.

.gui-toast {
  --_duration: 3s;
  --_travel-distance: 0;

  will-change: transform;
  animation: 
    fade-in .3s ease,
    slide-in .3s ease,
    fade-out .3s ease var(--_duration);
}

@media (prefers-reduced-motion: no-preference) {
  .gui-toast {
    --_travel-distance: 5vh;
  }
}

JavaScript

Com os estilos e o HTML acessível ao leitor de tela prontos, o JavaScript é necessário para orquestrar a criação, adição e destruição de toasts com base em eventos do usuário. A experiência do desenvolvedor com o componente de toast precisa ser mínima e fácil de começar, como esta:

import Toast from './toast.js'

Toast('My first toast')

Como criar o grupo e os toasts

Quando o módulo de toast é carregado do JavaScript, ele precisa criar um contêiner de toast e adicioná-lo à página. Escolhi adicionar o elemento antes de body. Isso vai evitar problemas de empilhamento de z-index, já que o contêiner está acima do contêiner de todos os elementos do corpo.

const init = () => {
  const node = document.createElement('section')
  node.classList.add('gui-toast-group')

  document.firstElementChild.insertBefore(node, document.body)
  return node
}

Captura de tela do grupo de toast entre as tags head e body.

A função init() é chamada internamente no módulo, armazenando o elemento como Toaster:

const Toaster = init()

A criação de elementos HTML de toast é feita com a função createToast(). A função exige um texto para o toast, cria um elemento <output>, o enfeita com algumas classes e atributos, define o texto e retorna o nó.

const createToast = text => {
  const node = document.createElement('output')
  
  node.innerText = text
  node.classList.add('gui-toast')
  node.setAttribute('role', 'status')

  return node
}

Gerenciar um ou vários toasts

O JavaScript agora adiciona um contêiner ao documento para conter notificações e está pronto para adicionar notificações criadas. A função addToast() orquestra o processamento de um ou vários toasts. Primeiro, verifique o número de toasts e se o movimento está correto. Depois, use essas informações para anexar o toast ou fazer uma animação sofisticada para que os outros toasts pareçam "abrir espaço" para o novo.

const addToast = toast => {
  const { matches:motionOK } = window.matchMedia(
    '(prefers-reduced-motion: no-preference)'
  )

  Toaster.children.length && motionOK
    ? flipToast(toast)
    : Toaster.appendChild(toast)
}

Ao adicionar o primeiro toast, o Toaster.appendChild(toast) adiciona um toast à página, acionando as animações CSS: animação de entrada, espera 3s e animação de saída. flipToast() é chamado quando há brindes existentes, usando uma técnica chamada FLIP por Paul Lewis. A ideia é calcular a diferença nas posições do contêiner antes e depois da adição da nova mensagem. Pense em marcar onde a torradeira está agora, onde ela vai ficar e, em seguida, animar de onde ela estava para onde ela está.

const flipToast = toast => {
  // FIRST
  const first = Toaster.offsetHeight

  // add new child to change container size
  Toaster.appendChild(toast)

  // LAST
  const last = Toaster.offsetHeight

  // INVERT
  const invert = last - first

  // PLAY
  const animation = Toaster.animate([
    { transform: `translateY(${invert}px)` },
    { transform: 'translateY(0)' }
  ], {
    duration: 150,
    easing: 'ease-out',
  })
}

A grade CSS faz o levantamento do layout. Quando uma nova notificação é adicionada, a grade a coloca no início e a espaça com as outras. Enquanto isso, uma animação da Web é usada para animar o contêiner da posição antiga.

Reunindo todo o JavaScript

Quando Toast('my first toast') é chamado, um toast é criado, adicionado à página (talvez até o contêiner seja animado para acomodar o novo toast), uma promise é retornada e o toast criado é monitorado para conclusão da animação CSS (as três animações de keyframe) para resolução da promise.

const Toast = text => {
  let toast = createToast(text)
  addToast(toast)

  return new Promise(async (resolve, reject) => {
    await Promise.allSettled(
      toast.getAnimations().map(animation => 
        animation.finished
      )
    )
    Toaster.removeChild(toast)
    resolve() 
  })
}

A parte confusa desse código está na função Promise.allSettled() e no mapeamento toast.getAnimations(). Como usei várias animações de keyframe para o toast, para saber com certeza que todas terminaram, cada uma precisa ser solicitada do JavaScript e cada uma das promessas finished observadas para conclusão. allSettled funciona para nós, resolvendo-se como concluído quando todas as promessas foram cumpridas. Usar await Promise.allSettled() significa que a próxima linha de código pode remover o elemento com segurança e presumir que o toast concluiu o ciclo de vida. Por fim, chamar resolve() cumpre a promessa de Toast de alto nível para que os desenvolvedores possam limpar ou fazer outros trabalhos depois que o toast for mostrado.

export default Toast

Por fim, a função Toast é exportada do módulo para que outros scripts possam importar e usar.

Como usar o componente Toast

Para usar o toast ou a experiência do desenvolvedor dele, importe a função Toast e chame-a com uma string de mensagem.

import Toast from './toast.js'

Toast('Wizard Rose added to cart')

Se o desenvolvedor quiser fazer um trabalho de limpeza ou algo assim depois que o toast for mostrado, ele poderá usar async e await.

import Toast from './toast.js'

async function example() {
  await Toast('Wizard Rose added to cart')
  console.log('toast finished')
}

Conclusão

Agora que você sabe como eu fiz, como você faria? 🙂

Vamos diversificar nossas abordagens e aprender todas as maneiras de criar na Web. Crie uma demonstração, me envie um tweet com o link, e eu vou adicionar à seção de remixes da comunidade abaixo.

Remixes da comunidade