Como criar um componente de aviso

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

Neste post, quero compartilhar ideias sobre como criar um componente de aviso. Teste a demonstração.

Demo

Se preferir vídeos, confira a versão desta postagem no YouTube:

Visão geral

Os avisos 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 o usuário sobre os resultados de uma ação.

Interações

As mensagens curtas são diferentes de notificações, alertas e solicitações porque não são interativas. Elas não são destinadas a serem dispensadas ou persistir. As notificações são destinadas a informações mais importantes, mensagens síncronas que exigem interação ou mensagens no nível do sistema, e não no nível da página. As notificações emergentes são mais passivas do que outras estratégias de aviso.

Marcação

O elemento <output> é uma boa escolha para o aviso porque ele é anunciado para leitores de tela. O HTML correto fornece uma base segura para aprimorarmos com JavaScript e CSS, e haverá muito JavaScript.

Um aviso

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

Ele pode ser mais inclusivo ao adicionar role="status". Isso fornecerá um substituto se o navegador não conceder aos elementos <output> o papel implícito (link em inglês) de acordo com a especificação.

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

Um contêiner de aviso

É possível mostrar mais de um aviso por vez. Um contêiner é usado para orquestrar vários avisos. Esse contêiner também processa a posição das notificações 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

Escolhi fixar os avisos inset-block-end da viewport. Se mais avisos forem adicionados, eles serão empilhados a partir da borda da tela.

Contêiner da GUI

O contêiner de avisos faz todo o trabalho de layout para apresentar avisos. Ele é fixed para a viewport 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 a um elemento .gui-toast-container.

Além de se posicionar dentro da viewport, o contêiner de avisos é um conjunto de grade que pode alinhar e distribuir avisos. Os itens são centralizados como um grupo com justify-content e centralizados individualmente com justify-items. Coloque um pouco de gap para que os avisos não toquem.

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

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

Toast da GUI

Uma mensagem mostrada individualmente tem 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 as notificações cresçam mais de 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 a se adaptar às configurações e interações do usuário.

Contêiner de aviso

As notificações não são interativas. Tocar ou deslizar nelas não faz nada, mas elas consomem eventos de ponteiro. Impeça que as notificações roubem cliques com o CSS a seguir.

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

Toast da GUI

Dê aos avisos 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 aviso vai aparecer com uma animação ao entrar na tela. Para acomodar o movimento reduzido, defina os valores de translate como 0 por padrão, mas atualize o valor do 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 fazem o aviso viajar uma distância.

Estes são os frames-chave usados para a animação de aviso. O CSS controlará a entrada, a espera e a saída da notificação, tudo em uma única animação.

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

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

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

O elemento de aviso vai configurar as variáveis e orquestrar 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 para leitores de tela prontos, o JavaScript é necessário para orquestrar a criação, adição e destruição de notificações pop-up com base nos eventos do usuário. A experiência do desenvolvedor com o componente de aviso precisa ser mínima e fácil de começar, como esta:

import Toast from './toast.js'

Toast('My first toast')

Criar o grupo de avisos e os avisos

Quando o módulo de aviso é carregado do JavaScript, ele precisa criar um contêiner de aviso e adicioná-lo à página. Decidi adicionar o elemento antes de body. Isso vai dificultar a ocorrência de problemas de empilhamento de z-index, já que o contêiner está acima do contêiner para 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 avisos 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 avisos é feita com a função createToast(). A função exige algum texto para o aviso, 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
}

Como gerenciar um ou vários avisos

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 uma ou várias notificações. Primeiro, verifique o número de avisos e se o movimento está correto. Em seguida, use essas informações para anexar o aviso ou fazer uma animação sofisticada para que os outros avisos pareçam "criar espaço" para o novo aviso.

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 aviso, Toaster.appendChild(toast) adiciona um aviso à página acionando as animações CSS: iniciar a animação, aguardar 3s, encerrar a animação. flipToast() é chamado quando há avisos ativos, empregando uma técnica chamada FLIP por Paul Lewis. A ideia é calcular a diferença nas posições do contêiner, antes e depois de adicionar o novo aviso. É como marcar onde o Toaster está agora, onde ele vai estar e, em seguida, animar de onde ele estava para onde 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 eleva o layout. Quando um novo aviso é adicionado, a grade o coloca no início e o espaça com os outros. Enquanto isso, uma animação da Web é usada para animar o contêiner da posição antiga.

Como juntar todo o JavaScript

Quando Toast('my first toast') é chamado, um aviso é criado e adicionado à página (talvez até o contêiner seja animado para acomodar o novo aviso), uma promessa é retornada e a notificação criada é assistida à conclusão da animação CSS (as três animações de frame-chave) para a resolução da promessa.

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() 
  })
}

Achei que 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 a mensagem, para saber com certeza que todas foram concluídas, cada uma delas precisa ser solicitada pelo JavaScript e cada uma das finished promessas observadas para conclusão. allSettled faz isso para nós, resolvendo-se como completo quando todas as promessas forem cumpridas. O uso de await Promise.allSettled() significa que a próxima linha de código pode remover o elemento com segurança e presumir que a mensagem já concluiu o ciclo de vida. Por fim, chamar resolve() atende à promessa de aviso de alto nível para que os desenvolvedores possam limpar ou fazer outro trabalho quando a notificação for exibida.

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

O uso da notificação ou da experiência do desenvolvedor dela é feito importando a função Toast e chamando-a com uma string de mensagem.

import Toast from './toast.js'

Toast('Wizard Rose added to cart')

Se o desenvolvedor quiser limpar o trabalho ou o que for, depois que a mensagem for mostrada, ele poderá usar o modo assíncrono 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, envie um tweet para mim (link em inglês) e eu vou adicionar o conteúdo à seção de remixes da comunidade abaixo.

Remixes da comunidade