Una panoramica di base su come creare un componente toast adattivo e accessibile.
In questo post voglio condividere il mio pensiero su come creare un componente di notifica. Prova la demo.
Se preferisci i video, ecco una versione di questo post su YouTube:
Panoramica
I toast sono messaggi brevi non interattivi, passivi e asincroni per gli utenti. In genere vengono utilizzati come pattern di feedback dell'interfaccia per informare l'utente sui risultati di un'azione.
Interazioni
I toast sono diversi da notifiche, avvisi e richieste perché non sono interattivi, non sono pensati per essere chiusi o rimanere sullo schermo. Le notifiche riguardano informazioni più importanti, messaggi sincroni che richiedono interazione o messaggi a livello di sistema (anziché a livello di pagina). I toast sono più passivi rispetto ad altre strategie di avviso.
Segni e linee
L'elemento
<output>
è una buona scelta per il toast perché viene annunciato ai lettori
dello schermo. L'HTML corretto fornisce una base sicura per il miglioramento con JavaScript e
CSS e ci sarà molto JavaScript.
Un brindisi
<output class="gui-toast">Item added to cart</output>
Può essere più
inclusivo
aggiungendo role="status"
. In questo modo viene fornito un
fallback se il browser non assegna agli elementi <output>
il ruolo
implicito
come da specifica.
<output role="status" class="gui-toast">Item added to cart</output>
Un contenitore di toast
È possibile visualizzare più di un toast alla volta. Per orchestrare più avvisi, viene utilizzato un contenitore. Questo contenitore gestisce anche la posizione dei toast sullo schermo.
<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>
Layout
Ho scelto di bloccare i toast nella
inset-block-end
della finestra e, se vengono aggiunti altri toast, questi si impilano dal bordo dello schermo.
Contenitore GUI
Il contenitore dei toast esegue tutto il lavoro di layout per la presentazione dei toast. È
fixed
alla finestra e utilizza la proprietà logica
inset
per specificare a quali
bordi ancorarsi, oltre a un po' di padding
dallo stesso bordo block-end
.
.gui-toast-group {
position: fixed;
z-index: 1;
inset-block-end: 0;
inset-inline: 0;
padding-block-end: 5vh;
}
Oltre a posizionarsi all'interno dell'area visibile, il contenitore dei toast è un contenitore a griglia che può allineare e distribuire i toast. Gli elementi sono centrati come
gruppo con justify-content
e centrati singolarmente con justify-items
.
Aggiungi un po' di gap
in modo che i toast non si tocchino.
.gui-toast-group {
display: grid;
justify-items: center;
justify-content: center;
gap: 1vh;
}
GUI Toast
Un singolo toast ha alcuni padding
, alcuni angoli più morbidi con
border-radius
,
e una funzione min()
per
aiutare a dimensionare i toast su dispositivi mobili e computer. La dimensione adattabile nel seguente CSS
impedisce ai toast di diventare più larghi del 90% dell'area visibile o
25ch
.
.gui-toast {
max-inline-size: min(25ch, 90vw);
padding-block: .5ch;
padding-inline: 1ch;
border-radius: 3px;
font-size: 1rem;
}
Stili
Una volta impostati il layout e il posizionamento, aggiungi il CSS che ti aiuta ad adattarti alle impostazioni e alle interazioni dell'utente.
Contenitore dei toast
I toast non sono interattivi, toccarli o scorrerli non fa nulla, ma al momento utilizzano gli eventi puntatore. Impedisci ai toast di intercettare i clic con il seguente CSS.
.gui-toast-group {
pointer-events: none;
}
GUI Toast
Assegna ai toast un tema adattivo chiaro o scuro con proprietà personalizzate, HSL e una query multimediale delle preferenze.
.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%;
}
}
Animazione
Un nuovo toast dovrebbe presentarsi con un'animazione quando entra nella schermata.
L'adattamento al movimento ridotto viene eseguito impostando i valori di translate
su 0
per
impostazione predefinita, ma aggiornando il valore di movimento a una lunghezza in una query
multimediale di preferenza di movimento . Tutti vedono un'animazione, ma solo alcuni utenti vedono il messaggio di notifica spostarsi
per una certa distanza.
Ecco i fotogrammi chiave utilizzati per l'animazione del toast. Il CSS controllerà l'ingresso, l'attesa e l'uscita della notifica di tipo toast, tutto in un'unica animazione.
@keyframes fade-in {
from { opacity: 0 }
}
@keyframes fade-out {
to { opacity: 0 }
}
@keyframes slide-in {
from { transform: translateY(var(--_travel-distance, 10px)) }
}
L'elemento toast configura quindi le variabili e orchestra i fotogrammi chiave.
.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
Con gli stili e il codice HTML accessibile allo screen reader pronti, è necessario JavaScript per orchestrare la creazione, l'aggiunta e l'eliminazione dei toast in base agli eventi utente. L'esperienza dello sviluppatore del componente toast deve essere minima e facile da usare, come in questo esempio:
import Toast from './toast.js'
Toast('My first toast')
Creazione del gruppo di toast e dei toast
Quando il modulo di notifica viene caricato da JavaScript, deve creare un contenitore di notifiche
e aggiungerlo alla pagina. Ho scelto di aggiungere l'elemento prima di body
, in questo modo
i problemi di impilamento di z-index
sono improbabili perché il contenitore si trova sopra il contenitore di
tutti gli elementi del corpo.
const init = () => {
const node = document.createElement('section')
node.classList.add('gui-toast-group')
document.firstElementChild.insertBefore(node, document.body)
return node
}
La funzione init()
viene chiamata internamente al modulo, memorizzando l'elemento
come Toaster
:
const Toaster = init()
La creazione dell'elemento HTML toast viene eseguita con la funzione createToast()
. La
funzione richiede del testo per il toast, crea un elemento <output>
, lo
adorna con alcune classi e attributi, imposta il testo e restituisce il nodo.
const createToast = text => {
const node = document.createElement('output')
node.innerText = text
node.classList.add('gui-toast')
node.setAttribute('role', 'status')
return node
}
Gestire una o più notifiche popup
JavaScript ora aggiunge un contenitore al documento per contenere i toast ed è
pronto per aggiungere i toast creati. La funzione addToast()
coordina la gestione di una
o più notifiche. Controllando prima il numero di toast e se il movimento è accettabile,
poi utilizzando queste informazioni per aggiungere il toast o eseguire un'animazione
in modo che gli altri toast sembrino "fare spazio" al nuovo toast.
const addToast = toast => {
const { matches:motionOK } = window.matchMedia(
'(prefers-reduced-motion: no-preference)'
)
Toaster.children.length && motionOK
? flipToast(toast)
: Toaster.appendChild(toast)
}
Quando aggiungi il primo messaggio di notifica, Toaster.appendChild(toast)
aggiunge un messaggio di notifica alla
pagina che attiva le animazioni CSS: animazione in entrata, attesa 3s
, animazione in uscita.
flipToast()
viene chiamato quando sono presenti toast esistenti, utilizzando una tecnica
chiamata FLIP da Paul
Lewis. L'idea è di calcolare la differenza
nelle posizioni del contenitore, prima e dopo l'aggiunta del nuovo messaggio di notifica.
Immagina di segnare la posizione attuale del Toaster e quella futura, poi
di animare il movimento dalla posizione iniziale a quella finale.
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',
})
}
La griglia CSS si occupa del layout. Quando viene aggiunto un nuovo toast, la griglia lo posiziona all'inizio e lo distanzia dagli altri. Nel frattempo, un'animazione web viene utilizzata per animare il container dalla vecchia posizione.
Mettere insieme tutto il codice JavaScript
Quando viene chiamato Toast('my first toast')
, viene creato un toast, aggiunto alla pagina
(forse anche il contenitore viene animato per ospitare il nuovo toast), viene restituita una
promessa
e il toast creato viene
monitorato per
il completamento dell'animazione CSS (le tre animazioni keyframe) per la risoluzione della 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()
})
}
La parte più confusa di questo codice si trova nella funzione Promise.allSettled()
e nella mappatura toast.getAnimations()
. Poiché ho utilizzato più animazioni keyframe
per il toast, per sapere con certezza che sono tutte terminate, ognuna deve essere
richiesta da JavaScript e ognuna delle
promesse finished
osservate per il completamento.
allSettled
funziona per noi, risolvendosi come completata una volta che tutte le sue promesse
sono state soddisfatte. L'utilizzo di await Promise.allSettled()
significa che la riga di codice successiva può rimuovere con certezza l'elemento e presupporre che il toast abbia completato il suo ciclo di vita. Infine, la chiamata a resolve()
soddisfa la promessa di Toast di alto livello, quindi
gli sviluppatori possono eseguire la pulizia o altre operazioni una volta visualizzato il toast.
export default Toast
Infine, la funzione Toast
viene esportata dal modulo per essere importata e utilizzata da altri script.
Utilizzo del componente Toast
L'utilizzo del toast o dell'esperienza di sviluppo del toast avviene importando la
funzione Toast
e chiamandola con una stringa di messaggio.
import Toast from './toast.js'
Toast('Wizard Rose added to cart')
Se lo sviluppatore vuole eseguire operazioni di pulizia o altro, dopo la visualizzazione della notifica può utilizzare async e await.
import Toast from './toast.js'
async function example() {
await Toast('Wizard Rose added to cart')
console.log('toast finished')
}
Conclusione
Ora che sai come ho fatto, come faresti tu?‽ 🙂
Diversifichiamo i nostri approcci e impariamo tutti i modi per creare sul web. Crea una demo, inviami un tweet con i link e la aggiungerò alla sezione dei remix della community qui sotto.
Remix della community
- @_developit con HTML/CSS/JS: demo e codice
- Joost van der Schee con HTML/CSS/JS: demo e codice