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

As notificações curtas não são interativas, são 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 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). 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>

É possível tornar mais inclusivo adicionando role="status". Isso fornece um fallback se o navegador não atribuir aos elementos <output> o papel implícito 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 uma mensagem ao mesmo tempo. Para orquestrar vários alertas, um contêiner é usado. 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 as notificações ao inset-block-end da viewport. Se mais notificações forem adicionadas, elas serão empilhadas a partir dessa 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. Adicione um pouco de gap para que as notificações não se 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 os intervalos entre os elementos filhos do aviso.

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 têm a mensagem aparecendo em uma distância.

Estes são os frames-chave usados para a animação de aviso. O CSS vai controlar a entrada, a espera e a saída da mensagem, 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)) }
}

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. Escolhi adicionar o elemento antes de body. Isso vai tornar os problemas de empilhamento de z-index improváveis, 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 a mensagem, cria um elemento <output>, o decora 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 uma ou várias mensagens de aviso

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 notificações e se o movimento está ok, depois use essas informações para anexar a notificação ou fazer uma animação legal para que as outras notificações apareçam para "abrir espaço" para a nova.

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: animação de entrada, espera 3s, animação de saída. 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. Pense nisso como marcar onde a torradeira está agora, onde ela vai estar 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 eleva o layout. Quando um novo aviso aparece, a grade o coloca no início e o separa dos outros. Enquanto isso, uma animação da Web é usada para animar o contêiner da posição antiga.

Como juntar tudo no JavaScript

Quando Toast('my first toast') é chamado, uma mensagem popover é criada e adicionada à página (talvez até o contêiner seja animado para acomodar a nova mensagem popover), uma promessa é retornada e a mensagem popover criada é monitorada para a conclusão da animação CSS (as três animações de keyframe) para resolução de 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() 
  })
}

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() cumpre a promessa de aviso de alto nível para que os desenvolvedores possam limpar ou fazer outro trabalho depois que o aviso 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 aviso ou a experiência do desenvolvedor, 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 limpar o trabalho ou algo assim, 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 links para mim e vou adicionar à seção de remixes da comunidade abaixo.

Remixes da comunidade