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.
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;
}
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;
}
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;
}
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
}
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
- @_developit com HTML/CSS/JS: demonstração e código
- Joost van der Schee com HTML/CSS/JS: demonstração e código