Toast-Komponente erstellen

Eine grundlegende Übersicht darüber, wie Sie eine adaptive und barrierefreie Toast-Komponente erstellen.

In diesem Beitrag möchte ich meine Überlegungen zum Erstellen einer Toast-Komponente teilen. Demo ansehen

Demo

Wenn du lieber ein Video ansehen möchtest, 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 Schnittstellen-Feedbackmuster verwendet, um den Nutzer über die Ergebnisse einer Aktion zu informieren.

Interaktionen

Toasts unterscheiden sich von Benachrichtigungen, Warnungen und Aufforderungen, da sie nicht interaktiv sind. Sie sollen nicht geschlossen werden und auch nicht bestehen bleiben. Benachrichtigungen sind für wichtigere Informationen, synchrone Nachrichten, die eine Interaktion erfordern, oder Systemnachrichten (im Gegensatz zu Seitenebene) vorgesehen. Toasts sind passiver als andere Benachrichtigungsstrategien.

Markieren & Zeichnen

Das Element <output> ist eine gute Wahl für den Toast, da es Screenreadern vorgelesen wird. Korrektes HTML bietet eine sichere Grundlage für die Erweiterung mit JavaScript und CSS. Es wird viel JavaScript geben.

Ein Toast

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

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

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

Ein Toastbehälter

Es können mehrere Toasts gleichzeitig angezeigt werden. Zum Orchestrieren mehrerer Toasts wird ein Container verwendet. Dieser Container übernimmt auch die Position der Benachrichtigungen 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, Benachrichtigungen an der inset-block-end des Viewports anzupinnen. Wenn weitere Benachrichtigungen hinzugefügt werden, werden sie an dieser Bildschirmkante gestapelt.

GUI-Container

Der Toast-Container übernimmt die gesamte Layoutarbeit für die Darstellung von Toasts. Es ist fixed zum Viewport und verwendet die logische Property inset, um anzugeben, an welchen Rändern es angepinnt werden soll, sowie ein wenig padding vom selben block-end-Rand.

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

Screenshot mit der Größe und dem Padding des DevTools-Felds, die auf ein .gui-toast-container-Element gelegt werden.

Der Toast-Container wird nicht nur im Darstellungsbereich positioniert, sondern ist auch ein Grid-Container, mit dem sich Toasts ausrichten und verteilen lassen. Elemente werden als Gruppe mit justify-content und einzeln mit justify-items zentriert. Gib etwas gap hinzu, damit die Toasts sich nicht berühren.

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

Screenshot mit dem CSS-Grid-Overlay für die Toast-Gruppe, diesmal mit Hervorhebung des Abstands und der Lücken zwischen den untergeordneten Toast-Elementen.

GUI-Toast

Ein einzelner Toast hat einige padding, einige weichere Ecken mit border-radius und eine min()-Funktion, die bei der Größenanpassung für Mobilgeräte und Computer hilft. Die responsive Größe im folgenden CSS 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 dem Padding und dem Grenzradius.

Stile

Nachdem Sie das Layout und die Positionierung festgelegt haben, fügen Sie CSS hinzu, um die Anpassung an Nutzereinstellungen und ‑interaktionen zu erleichtern.

Toast-Container

Toasts sind nicht interaktiv. Wenn Sie darauf tippen oder wischen, passiert nichts. Allerdings werden derzeit Zeigerereignisse verarbeitet. Mit dem folgenden CSS können Sie verhindern, dass durch die Pop-up-Benachrichtigungen Klicks verloren gehen.

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

GUI-Toast

Weisen Sie den Toasts ein helles oder dunkles adaptives Design mit benutzerdefinierten Eigenschaften, HSL und einer Media-Query für die bevorzugte Farbe zu.

.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 neuer Toast sollte mit einer Animation auf dem Bildschirm erscheinen. Um die reduzierte Bewegung zu berücksichtigen, werden translate-Werte standardmäßig auf 0 festgelegt. Der Bewegungs-Wert wird jedoch in einer Media-Anfrage mit Bewegungspräferenz auf eine Länge aktualisiert. Alle Nutzer sehen eine Animation, aber nur bei einigen Nutzern bewegt sich der Toast über eine bestimmte Distanz.

Hier sind die Keyframes, die für die Toast-Animation verwendet werden. Mit CSS werden das Einblenden, die Wartezeit und das Ausblenden 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 Keyframes.

.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 das für Screenreader zugängliche HTML fertig sind, ist JavaScript erforderlich, um das Erstellen, Hinzufügen und Löschen von Toasts basierend auf Nutzerereignissen zu koordinieren. Die Entwicklerfreundlichkeit der Toast-Komponente sollte minimal sein und der Einstieg sollte einfach sein, z. B. so:

import Toast from './toast.js'

Toast('My first toast')

Toast-Gruppe 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 eingefügt. Dadurch sind z-index-Stapelprobleme 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 für das Modul aufgerufen und speichert das Element als Toaster:

const Toaster = init()

Das Toast-HTML-Element wird mit der Funktion createToast() erstellt. Die Funktion erfordert etwas Text für den Toast, erstellt ein <output>-Element, versieht es mit einigen Klassen und Attributen, 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
}

Einen oder mehrere Toasts verwalten

JavaScript fügt dem Dokument jetzt einen Container für die Aufnahme von Benachrichtigungen hinzu und ist bereit, erstellte Benachrichtigungen hinzuzufügen. Die Funktion addToast() orchestriert die Verarbeitung eines oder mehrerer Toasts. Zuerst wird die Anzahl der Benachrichtigungen geprüft und ob Bewegung in Ordnung ist. Dann werden diese Informationen verwendet, um die Benachrichtigung entweder anzuhängen oder eine ausgefeilte Animation zu erstellen, damit die anderen Benachrichtigungen Platz für die neue Benachrichtigung schaffen.

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

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

Beim Hinzufügen des ersten Toasts fügt Toaster.appendChild(toast) einen Toast auf der Seite ein, der die CSS-Animationen auslöst: Einblenden, 3s abwarten, Ausblenden. flipToast() wird aufgerufen, wenn bereits Toasts vorhanden sind. Dabei wird eine von Paul Lewis entwickelte Technik namens FLIP verwendet. Die Idee ist, die Differenz der Positionen 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 und wo er sich hinbewegen soll. Dann animieren Sie die Bewegung von der aktuellen Position zur Zielposition.

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 übernimmt die Gestaltung des Layouts. Wenn ein neuer Toast hinzugefügt wird, platziert das Raster ihn am Anfang und sorgt für den richtigen Abstand zu den anderen. In der Zwischenzeit wird eine Webanimation verwendet, um den Container von der alten Position aus zu animieren.

JavaScript zusammenfügen

Wenn Toast('my first toast') aufgerufen wird, wird ein Hinweis erstellt, der der Seite hinzugefügt wird (möglicherweise wird sogar der Container animiert, um den neuen Hinweis aufzunehmen). Es wird ein Promise zurückgegeben und der erstellte Hinweis wird beobachtet, bis die CSS-Animation abgeschlossen ist (die drei Keyframe-Animationen), damit das Promise aufgelöst 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() 
  })
}

Der verwirrende Teil dieses Codes liegt meiner Meinung nach in der Funktion Promise.allSettled() und der Zuordnung toast.getAnimations(). Da ich mehrere Keyframe-Animationen für den Toast verwendet habe, muss jede von JavaScript angefordert und jede ihrer finished-Promises auf Abschluss beobachtet werden, damit ich sicher sein kann, dass alle abgeschlossen sind. allSettled wird automatisch als abgeschlossen markiert, sobald alle zugehörigen Versprechen erfüllt wurden. Wenn Sie await Promise.allSettled() verwenden, kann das Element in der nächsten Codezeile problemlos entfernt werden, da davon ausgegangen wird, dass der Toast-Benachrichtigungsvorgang abgeschlossen ist. Durch den Aufruf von resolve() wird das Toast-Promise auf hoher Ebene erfüllt, sodass Entwickler nach dem Anzeigen des Toasts Aufräumarbeiten oder andere Aufgaben ausführen können.

export default Toast

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

Toast-Komponente verwenden

Um den Toast oder die Entwicklerfreundlichkeit des Toasts zu nutzen, importieren Sie die Funktion Toast und rufen sie mit einem Nachrichtenstring auf.

import Toast from './toast.js'

Toast('Wizard Rose added to cart')

Wenn der Entwickler nach dem Anzeigen des Toasts Aufräumarbeiten oder Ähnliches durchfü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

Jetzt wissen Sie, wie ich es gemacht habe. Wie würden Sie vorgehen? 🙂

Wir möchten unsere Ansätze diversifizieren und alle Möglichkeiten kennenlernen, die das Web bietet. Erstelle eine Demo, schick mir einen Tweet mit den Links und ich füge sie unten im Bereich „Community-Remixe“ hinzu.

Community-Remixe