Creazione di un componente toast

Una panoramica di base su come creare un componente di notifica adattabile 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 modello di feedback dell'interfaccia per informare l'utente circa i 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ù inclusiva aggiungendo role="status". Questo fornisce un valore alternativo se il browser non assegna agli elementi <output> il ruolo implicito in base alle specifiche.

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

Un contenitore di toast

È possibile visualizzare più di un messaggio popup alla volta. Per orchestrare più popup, viene utilizzato un contenitore. Questo contenitore gestisce anche la posizione delle notifiche 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 le notifiche popup sul canto inset-block-end del viewport e, se vengono aggiunte altre notifiche popup, queste si impileranno da quel 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 a quali bordi 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. 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;
}

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

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 responsive nel seguente CSS impediscono ai popup di diventare più larghi del 90% dell'area visibile o di superare 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 il CSS che ti aiuta ad adattarti alle impostazioni e alle interazioni dell'utente.

Contenitore di toast

Le notifiche popup non sono interattive, quindi non succede nulla se le tocchi o scorri, ma al momento utilizzano gli eventi del cursore. Impedisci ai popup di rubare i clic con il seguente CSS.

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

Toast della 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 visualizzano un'animazione, ma solo alcuni utenti vedono il popup percorrere una distanza.

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

Gestire uno o più popup

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à in seguito, 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 messaggio popup, aggiunto alla pagina (forse anche il contenitore è animato per adattarsi al nuovo messaggio popup), viene restituita una promessa e il messaggio popup creato viene monitorato per verificare il completamento dell'animazione CSS (le tre animazioni delle 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 è nella funzione Promise.allSettled() e nella mappatura toast.getAnimations(). Poiché ho utilizzato più animazioni di keyframe per il messaggio popup, per sapere con certezza che tutte sono terminate, ciascuna deve essere richiesta da JavaScript e ciascuna delle sue promesse finished deve essere osservata per il completamento. allSettled lo fa per noi, risolvendosi come completata una volta soddisfatte tutte le sue promesse. 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 sia terminato 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 essere importata e utilizzata da altri script.

Utilizzo del componente Toast

Per utilizzare il messaggio popup o l'esperienza dello sviluppatore del messaggio popup, importa la funzioneToast e chiamala 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 che il messaggio popup è stato visualizzato, 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? 🙂

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