Toast-Komponente erstellen

Eine grundlegende Übersicht zum Erstellen einer adaptiven und barrierefreien Toast-Komponente.

In diesem Beitrag möchte ich meine Gedanken dazu teilen, wie man eine Toast-Komponente erstellt. Demo ansehen

Demo

Wenn du lieber ein Video ansiehst, findest du hier eine YouTube-Version dieses Beitrags:

Übersicht

Toasts sind nicht interaktive, passive und asynchrone Kurznachrichten für Nutzer. Sie werden in der Regel als Feedbackmuster für die Benutzeroberfläche verwendet, um Nutzer über die Ergebnisse einer Aktion zu informieren.

Interaktionen

Im Gegensatz zu Benachrichtigungen, Warnungen und Aufforderungen sind Toasts nicht interaktiv. Sie können nicht geschlossen oder beibehalten werden. Benachrichtigungen sind für wichtigere Informationen, synchrone Nachrichten, die eine Interaktion erfordern, oder Nachrichten auf Systemebene (im Gegensatz zu Seitenebene) gedacht. Toasts sind passiver als andere Benachrichtigungsstrategien.

Markieren & Zeichnen

Das Element <output> ist eine gute Wahl für das Toast-Element, da es von Screenreadern angesagt wird. Korrekte HTML-Code bietet eine solide Grundlage, die wir mit JavaScript und CSS erweitern können. Und es wird viel JavaScript geben.

Ein Toast

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

Sie können inklusiver werden, indem Sie role="status" hinzufügen. Dies dient als Fallback, wenn der Browser <output>-Elementen nicht die implicite Rolle gemäß der Spezifikation zuweist.

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

Ein Toastbehälter

Es können mehrere Toast-Nachrichten gleichzeitig angezeigt werden. Um mehrere Toasts zu orchestrieren, wird ein Container verwendet. Dieser Container verwaltet auch die Position der Toast-Nachrichten auf dem Bildschirm.

<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

Ich habe mich entschieden, Toasts an der inset-block-end des Darstellungsbereichs anzupinnen. Wenn weitere Toasts hinzugefügt werden, werden sie von diesem Bildschirmrand aus gestapelt.

GUI-Container

Der Toast-Container übernimmt die gesamte Layoutarbeit für die Darstellung von Toasts. Sie ist fixed zum Viewport und verwendet die Logikeigenschaft inset, um anzugeben, an welchen Rändern sie angepinnt werden soll, sowie ein wenig padding von derselben block-end-Kante.

.gui-toast-group {
  position: fixed;
  z-index: 1;
  inset-block-end: 0;
  inset-inline: 0;
  padding-block-end: 5vh;
}

Screenshot mit DevTools-Boxgröße und ‑Abstand, die auf einem .gui-toast-container-Element überlagert sind

Der Toast-Container wird nicht nur im Darstellungsbereich positioniert, sondern ist auch ein Rastercontainer, mit dem Toasts ausgerichtet und verteilt werden können. Elemente werden mit justify-content als Gruppe und mit justify-items einzeln zentriert. Geben Sie ein wenig gap hinzu, damit sich die Toasts nicht berühren.

.gui-toast-group {
  display: grid;
  justify-items: center;
  justify-content: center;
  gap: 1vh;
}

Screenshot mit dem CSS-Raster-Overlay auf der Toast-Gruppe, diesmal mit hervorgehobenem Abstand und Lücken zwischen den untergeordneten Toast-Elementen.

Toast-Meldung in der Benutzeroberfläche

Ein einzelnes Toast-Element hat einige padding, etwas abgerundete Ecken mit border-radius und eine min()-Funktion, die bei der Größenanpassung für Mobilgeräte und Computer hilft. Mit der responsiven Größe im folgenden CSS wird verhindert, dass Toasts breiter als 90% des Darstellungsbereichs oder 25ch werden.

.gui-toast {
  max-inline-size: min(25ch, 90vw);
  padding-block: .5ch;
  padding-inline: 1ch;
  border-radius: 3px;
  font-size: 1rem;
}

Screenshot eines einzelnen .gui-Toast-Elements mit dargestelltem Abstand und Rahmenradius

Stile

Nachdem Sie das Layout und die Position festgelegt haben, fügen Sie CSS hinzu, das die Anpassung an die Nutzereinstellungen und -interaktionen unterstützt.

Toast container

Toasts sind nicht interaktiv. Wenn Sie darauf tippen oder wischen, passiert nichts. Derzeit werden aber Cursorereignisse für sie erfasst. Mit dem folgenden CSS können Sie verhindern, dass die Toasts Klicks stehlen.

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

Toast-Meldung in der Benutzeroberfläche

Wählen Sie für die Toasts ein helles oder dunkles adaptives Design mit benutzerdefinierten Eigenschaften, HSL und einer Medienabfrage für die bevorzugte Darstellung aus.

.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%;
  }
}

Animation

Ein neues Toast sollte mit einer Animation präsentiert werden, wenn es auf dem Bildschirm erscheint. Wenn du weniger Bewegung zulassen möchtest, kannst du die translate-Werte standardmäßig auf 0 festlegen, den Bewegungswert aber in einer Medienabfrage mit Bewegungspräferenz auf eine bestimmte Länge aktualisieren . Alle sehen eine Animation, aber nur einige Nutzer sehen, dass die Meldung eine gewisse Strecke zurücklegt.

Hier sind die Keyframes, die für die Toast-Animation verwendet wurden. Mit CSS werden der Beginn, die Wartezeit und das Ende des Toasts in einer einzigen Animation gesteuert.

@keyframes fade-in {
  from { opacity: 0 }
}

@keyframes fade-out {
  to { opacity: 0 }
}

@keyframes slide-in {
  from { transform: translateY(var(--_travel-distance, 10px)) }
}

Das Toast-Element richtet dann die Variablen ein und orchestriert die Frames.

.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

Nachdem die Stile und die für Screenreader zugängliche HTML-Datei fertig sind, ist JavaScript erforderlich, um Toasts basierend auf Nutzerereignissen zu erstellen, hinzuzufügen und zu löschen. Die Entwicklerfreundlichkeit der Toast-Komponente sollte minimal und einfach zu erlernen sein, z. B. so:

import Toast from './toast.js'

Toast('My first toast')

Toastgruppe und Toasts erstellen

Wenn das Toast-Modul über JavaScript geladen wird, muss ein Toast-Container erstellt und der Seite hinzugefügt werden. Ich habe das Element vor body hinzugefügt. Dadurch sind Probleme beim Stapeln von z-index unwahrscheinlich, da sich der Container über dem Container für alle Body-Elemente befindet.

const init = () => {
  const node = document.createElement('section')
  node.classList.add('gui-toast-group')

  document.firstElementChild.insertBefore(node, document.body)
  return node
}

Screenshot der Toast-Gruppe zwischen den Head- und Body-Tags

Die Funktion init() wird intern im Modul aufgerufen und das Element wird als Toaster gespeichert:

const Toaster = init()

Das HTML-Element „Toast“ wird mit der Funktion createToast() erstellt. Die Funktion benötigt Text für das Toast-Element, erstellt ein <output>-Element, fügt ihm einige Klassen und Attribute hinzu, legt den Text fest und gibt den Knoten zurück.

const createToast = text => {
  const node = document.createElement('output')
  
  node.innerText = text
  node.classList.add('gui-toast')
  node.setAttribute('role', 'status')

  return node
}

Ein oder mehrere Toasts verwalten

JavaScript fügt dem Dokument jetzt einen Container für Toasts hinzu und ist bereit, erstellte Toasts hinzuzufügen. Die Funktion addToast() steuert die Verarbeitung einer oder mehrerer Toast-Meldungen. Zuerst wird die Anzahl der Toasts und die Bewegung geprüft. Anhand dieser Informationen wird dann entweder der Toast angehängt oder eine ausgefallene Animation ausgeführt, damit die anderen Toasts für den neuen Toast „Platz machen“.

const addToast = toast => {
  const { matches:motionOK } = window.matchMedia(
    '(prefers-reduced-motion: no-preference)'
  )

  Toaster.children.length && motionOK
    ? flipToast(toast)
    : Toaster.appendChild(toast)
}

Wenn Sie das erste Toast-Element hinzufügen, fügt Toaster.appendChild(toast) der Seite ein Toast-Element hinzu, das die CSS-Animationen auslöst: Einblenden, 3s warten, Ausblenden. flipToast() wird aufgerufen, wenn bereits Toasts vorhanden sind. Dabei wird eine von Paul Lewis entwickelte Methode namens FLIP verwendet. Es geht darum, den Unterschied zwischen der Position des Containers vor und nach dem Hinzufügen des neuen Toasts zu berechnen. Stellen Sie sich vor, Sie markieren, wo sich der Toaster gerade befindet, wo er sich später befinden wird, und animieren dann die Bewegung von der einen Position zur anderen.

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',
  })
}

Das CSS-Raster sorgt für die Aufhebung des Layouts. Wenn ein neues Toast-Element hinzugefügt wird, wird es im Raster an den Anfang gestellt und mit den anderen Elementen ausgerichtet. In der Zwischenzeit wird der Container mit einer Webanimation von der alten Position animiert.

JavaScript-Code zusammenführen

Wenn Toast('my first toast') aufgerufen wird, wird ein Toast erstellt und der Seite hinzugefügt (möglicherweise wird sogar der Container animiert, um den neuen Toast aufzunehmen). Ein Versprechen wird zurückgegeben und der erstellte Toast wird überwacht, bis die CSS-Animation (die drei ‑Frame-Animationen) abgeschlossen ist, damit das Versprechen erfüllt werden kann.

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() 
  })
}

Ich fand den Code für die Promise.allSettled()-Funktion und die toast.getAnimations()-Zuordnung etwas verwirrend. Da ich für die Toast-Meldung mehrere Keyframe-Animationen verwendet habe, muss jede davon von JavaScript angefordert und jedes finished-Versprechen auf Abschluss geprüft werden, damit ich sicher sein kann, dass alle abgeschlossen sind. allSettled erfüllt diese Anforderung. Sobald alle Versprechen erfüllt sind, wird es als abgeschlossen aufgelöst. Wenn Sie await Promise.allSettled() verwenden, kann das Element in der nächsten Codezeile sicher entfernt werden, da davon ausgegangen wird, dass der Toast seinen Lebenszyklus abgeschlossen hat. Durch den Aufruf von resolve() wird das Toast-Versprechen auf hoher Ebene erfüllt, sodass Entwickler nach dem Einblenden des Toasts Aufräumarbeiten durchführen oder andere Aufgaben erledigen können.

export default Toast

Schließlich wird die Toast-Funktion aus dem Modul exportiert, damit sie von anderen Scripts importiert und verwendet werden kann.

Toast-Komponente verwenden

Wenn Sie die Funktion „Toast“ oder die Entwicklerfreundlichkeit von „Toast“ verwenden möchten, importieren Sie die Funktion Toast und rufen Sie sie mit einem Nachrichtenstring auf.

import Toast from './toast.js'

Toast('Wizard Rose added to cart')

Wenn der Entwickler nach dem Einblenden des Toasts noch Aufräumarbeiten oder ähnliches ausführen möchte, kann er „async“ und await verwenden.

import Toast from './toast.js'

async function example() {
  await Toast('Wizard Rose added to cart')
  console.log('toast finished')
}

Fazit

Wie würden Sie das machen?

Lassen Sie uns unsere Ansätze diversifizieren und alle Möglichkeiten kennenlernen, wie Sie im Web entwickeln können. Erstelle eine Demo, tweete mir Links und ich füge sie unten in den Abschnitt „Community-Remixe“ hinzu.

Remixe der Community