Creazione di un componente toast

Una panoramica di base su come creare un componente Toast adattivo e accessibile.

In questo post voglio condividere alcune idee su come creare un componente di tipo toast. Prova la demo.

Demo

Se preferisci i video, ecco una versione di questo post su YouTube:

Panoramica

I popup 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

A differenza di notifiche, avvisi e prompt, i popup non sono interattivi e non sono destinati a essere ignorati o a rimanere attivi. Le notifiche sono destinate a informazioni più importanti, messaggi sincroni che richiedono interazione o messaggi a livello di sistema (anziché a livello di pagina). I popup sono più passivi rispetto ad altre strategie di avviso.

Segni e linee

L'elemento <output> è una buona scelta per il messaggio popup perché viene annunciato agli screen reader. L'HTML corretto fornisce una base sicura da migliorare 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". Questo fornisce un valore alternativo se il browser non assegna agli elementi <output> il ruolo implicito come da specifiche.

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

Un contenitore dei toast

È possibile visualizzare più di un messaggio popup alla volta. Per orchestrare più toast, si usa un container. 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 fissare i toast su inset-block-end dell'area visibile e, se ne vengono aggiunti altri, si impilano dal bordo dello schermo.

Contenitore GUI

Il contenitore dei toast gestisce tutto il layout per la presentazione dei toast. È fixed al viewport e utilizza la proprietà logica inset per specificare i bordi a cui bloccarsi, 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;
}

Screenshot con le dimensioni e i margini della casella di DevTools sovrapposti a un elemento .gui-toast-container.

Oltre a posizionarsi all'interno dell'area visibile, il contenitore di messaggi popup è un contenitore della griglia che può allineare e distribuire i messaggi popup. Gli elementi vengono centrati come gruppo con justify-content e singolarmente con justify-items. Metti 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;
}

Screenshot con la sovrapposizione della griglia CSS sul gruppo di notifiche, questa volta
in evidenza lo spazio e gli spazi tra gli elementi secondari della notifica.

Toast della GUI

Un singolo messaggio popup ha alcuni padding, alcuni angoli più morbidi con border-radius, e una funzione min() per aiutare a impostare le dimensioni per dispositivi mobili e computer. Le dimensioni adattabili nel seguente CSS impediscono che i toast aumentino di oltre il 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;
}

Screenshot di un singolo elemento .gui-toast, con spaziatura interna e raggio del bordo visualizzati.

Stili

Dopo aver impostato il layout e il posizionamento, aggiungi CSS che aiuta ad adattarti alle impostazioni e alle interazioni degli utenti.

Contenitore di toast

I toast non sono interattivi, il tocco o lo scorrimento non ha alcun effetto, ma al momento utilizzano eventi puntatore. Impedisci ai popup di rubare i clic con il seguente CSS.

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

Toast con GUI

Assegna ai popup un tema adattabile chiaro o scuro con proprietà personalizzate, HSL e una query sui media 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 messaggio popup dovrebbe apparire con un'animazione quando entra nella schermata. Per adattarsi a un movimento ridotto, imposta i valori translate su 0 per default, ma aggiorna il valore di movimento in base alla durata in una query media per le preferenze di movimento. Tutti gli utenti visualizzano un'animazione, ma solo alcuni vedono il popup che si sposta.

Di seguito sono riportati i fotogrammi chiave utilizzati per l'animazione del popup. Il CSS controllerà l'ingresso, l'attesa e l'uscita del messaggio popup, il 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 popup imposta 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

Una volta pronti gli stili e l'HTML accessibile agli screen reader, è necessario JavaScript per orchestrare la creazione, l'aggiunta e l'eliminazione di toast in base agli eventi dell'utente. L'esperienza dello sviluppatore del componente popup deve essere minima e facile da iniziare a utilizzare, ad 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 notifica e aggiungerlo alla pagina. Ho scelto di aggiungere l'elemento prima di body, in modo da ridurre al minimo i problemi di impilamento di z-index, poiché il contenitore è sopra il contenitore per 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
}

Screenshot del gruppo di popup tra i tag head e body.

La funzione init() viene chiamata internamente al modulo e memorizza l'elemento come Toaster:

const Toaster = init()

La creazione dell'elemento HTML di una notifica viene eseguita con la funzione createToast(). La funzione richiede del testo per il messaggio popup, crea un elemento <output>, lo decora 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
}

Gestione di uno o più toast

Ora JavaScript aggiunge un contenitore al documento per contenere i popup e può aggiungere i popup creati. La funzione addToast() orchestra la gestione di uno o più popup. Innanzitutto, controlla il numero di toast e se il movimento è corretto, poi utilizza queste informazioni per aggiungere il toast o creare un'animazione elaborata 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, Toaster.appendChild(toast) aggiunge un messaggio alla pagina attivando le animazioni CSS: animazione in, attesa 3s, animazione fuori. flipToast() viene chiamato quando sono presenti toast esistenti, utilizzando una tecnica chiamata FLIP di Paul Lewis. L'idea è calcolare la differenza tra le posizioni del contenitore prima e dopo l'aggiunta del nuovo messaggio popup. Pensa a come se dovessi contrassegnare la posizione attuale della tostiera e dove si troverà, quindi 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 gestisce il layout. Quando viene aggiunto un nuovo messaggio popup, la griglia lo inserisce all'inizio e lo inserisce tra gli altri. Nel frattempo, viene utilizzata un'animazione web per animare il contenitore dalla vecchia posizione.

Mettere insieme tutto il codice JavaScript

Quando viene chiamato Toast('my first toast'), viene creato un avviso popup, aggiunto alla pagina (forse anche il contenitore è animato per contenere il nuovo avviso popup), una promessa viene restituita e viene guardato l'avviso popup creato per il completamento dell'animazione CSS (le tre animazioni per i fotogrammi chiave) 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 è la funzione Promise.allSettled() e la mappatura toast.getAnimations(). Dato che per il toast ho usato diverse animazioni di fotogrammi chiave per sapere con sicurezza che tutte sono state terminate, ognuna deve essere richiesta da JavaScript e ognuna delle sue promesse di finished è stata osservata per il completamento. allSettled funziona in questo modo, risolvendosi come completa una volta che tutte le sue promesse sono state rispettate. L'utilizzo di await Promise.allSettled() significa che la riga di codice successiva può rimuovere l'elemento in tutta sicurezza e presuppone che il messaggio popup abbia completato il suo ciclo di vita. Infine, la chiamata a resolve() soddisfa la promessa di Toast di alto livello, in modo che gli sviluppatori possano eseguire la pulizia o altri lavori una volta visualizzato il messaggio.

export default Toast

Infine, la funzione Toast viene esportata dal modulo, per l'importazione e l'utilizzo da parte di altri script.

Utilizzo del componente Toast

Per utilizzare il messaggio popup o l'esperienza dello sviluppatore del messaggio popup, importa la funzione Toast e chiamala con una stringa di messaggio.

import Toast from './toast.js'

Toast('Wizard Rose added to cart')

Se lo sviluppatore vuole eseguire la pulizia o altro, dopo la visualizzazione del messaggio popup, può utilizzare asincrono 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? 🙂

Diversifichiamo i nostri approcci e impariamo tutti i modi per creare sul web. Crea una demo, twittami i link e io la aggiungerò alla sezione dei remix della community di seguito.

Remix della community