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