Toast-Komponente erstellen

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

In diesem Beitrag möchte ich darüber sprechen, wie man eine Toast-Komponente erstellt. Demo ansehen

Demo

Falls du lieber ein Video hast, findest du hier eine YouTube-Version dieses Beitrags:

Überblick

Toasts sind nicht interaktive, passive und asynchrone Kurznachrichten für Nutzer. Im Allgemeinen werden sie als Feedbackmuster auf der Benutzeroberfläche verwendet, um den Nutzer über die Ergebnisse einer Aktion zu informieren.

Interaktionen

Toasts unterscheiden sich von Benachrichtigungen, Warnmeldungen und Aufforderungen, weil sie nicht interaktiv sind. Sie sind nicht dazu gedacht, geschlossen oder dauerhaft angezeigt zu werden. Benachrichtigungen sind für wichtigere Informationen, synchrone Nachrichten, die eine Interaktion erfordern, oder Nachrichten auf Systemebene (im Gegensatz zu Nachrichten auf Seitenebene) gedacht. Toasts sind passiver als andere Benachrichtigungsstrategien.

Markup

Das Element <output> ist eine gute Wahl für den Toast, da er Screenreadern vorgelesen wird. Richtiger HTML-Code bietet eine sichere Basis für die Erweiterung von JavaScript und CSS, und es wird viel JavaScript geben.

Ein Toast

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

Es kann integrativer sein, wenn Sie role="status" hinzufügen. Dies bietet ein Fallback, wenn der Browser <output>-Elementen nicht die implizite Rolle gemäß der Spezifikation zuweist.

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

Einen Toastbehälter

Es können mehrere Toasts gleichzeitig angezeigt werden. Für die Orchestrierung mehrerer Toasts wird ein Container verwendet. Dieser Container steuert auch die Position der Toasts 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 dafür entschieden, Toasts im inset-block-end des Darstellungsbereichs anzupinnen. Wenn weitere Toasts hinzugefügt werden, werden sie von diesem Bildschirmrand aus gestapelt.

GUI-Container

Der Toastbehälter übernimmt das Layout für die Präsentation von Toasts. Sie ist fixed für den Darstellungsbereich und verwendet die logische Eigenschaft inset, um anzugeben, an welche Kanten angepinnt werden, und ein kleiner Wert von padding von derselben block-end-Kanten.

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

Screenshot mit Feldgröße und Abstand der Entwicklertools über einem .gui-toast-container-Element

Der Toastcontainer befindet sich nicht nur im Darstellungsbereich, sondern ist auch ein Rastercontainer, in dem Toasts ausgerichtet und verteilt werden können. Elemente werden als Gruppe mit justify-content und einzeln mit justify-items zentriert. Gib ein bisschen gap ein, damit Toasts nicht anfassen.

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

Screenshot mit dem CSS-Raster-Overlay in der Toast-Gruppe, in dem der Abstand und die Lücken zwischen untergeordneten Toast-Elementen hervorgehoben werden.

GUI-Toast

Ein einzelner Toast hat padding, einige weichere Ecken mit border-radius und eine min()-Funktion, um die Größe von Mobilgeräten und Computern anzupassen. Durch die responsive Größe im folgenden CSS wird verhindert, dass Toasts über 90% des Darstellungsbereichs oder von 25ch wachsen.

.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 angezeigtem Abstand und Rahmenradius

Stile

Wenn Layout und Positionierung festgelegt sind, fügen Sie CSS-Code hinzu, der die Anpassung an Nutzereinstellungen und Interaktionen erleichtert.

Toastbehälter

Toasts sind nicht interaktiv. Wenn Sie darauf tippen oder wischen, geschieht nichts, aber sie verbrauchen derzeit Zeigerereignisse. Mit dem folgenden CSS können Sie verhindern, dass die Toasts Klicks stehlen.

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

GUI-Toast

Sie können den Toasts ein helles oder dunkles adaptives Design mit benutzerdefinierten Eigenschaften, HSL und einer Medienabfrage für bevorzugte Medien geben.

.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 eingeblendet werden, sobald er auf dem Bildschirm erscheint. Um reduzierten Bewegungen Rechnung zu tragen, werden translate-Werte standardmäßig auf 0 gesetzt, aber in einer Medienabfrage mit Bewegungseinstellung auf eine Länge aktualisiert . Animationen werden zwar allen angezeigt, aber nur einige Nutzer haben eine bestimmte Strecke zurückgelegt.

Hier sehen Sie die Keyframes, die für die Toast-Animation verwendet werden. CSS steuert den Eingang, die Wartezeit und das Ende des Toasts in einer einzigen Animation.

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

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

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

Über das Toast-Element werden dann die Variablen eingerichtet und die Keyframes orchestriert.

.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

Sobald die Stile und der für den Screenreader zugängliche HTML bereit sind, wird JavaScript benötigt, um das Erstellen, Hinzufügen und Löschen von Toasts basierend auf Nutzerereignissen zu orchestrieren. Der Einstieg in die Entwicklung der Toast-Komponente sollte minimal und einfach sein:

import Toast from './toast.js'

Toast('My first toast')

Toast-Gruppe und Toast erstellen

Wenn das Toast-Modul aus JavaScript geladen wird, muss ein Toast-Container erstellt und der Seite hinzugefügt werden. Ich habe das Element vor body hinzugefügt. Dadurch sind z-index-Stapelprobleme unwahrscheinlich, da sich der Container für alle Textelemente über dem Container 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 Tags „head“ und „body“.

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

const Toaster = init()

Das HTML-Element eines Toasts wird mit der Funktion createToast() erstellt. Die Funktion benötigt Text für den Toast, erstellt ein <output>-Element, verleiht ihm einige Klassen und Attribute, 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 Toasts hinzu, den Sie erstellen können. Die Funktion addToast() orchestriert die Verarbeitung eines oder mehrerer Toasts. Überprüfen Sie zuerst die Anzahl der Toasts und ob eine Bewegung in Ordnung ist. Verwenden Sie diese Informationen dann, um entweder den Toast anzufügen oder eine schicke Animation zu erstellen, damit die anderen Toasts scheinbar Platz für den neuen Toast 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) der Seite einen Toast hinzu, wodurch die CSS-Animationen ausgelöst werden: Animieren, Warten auf 3s, Animieren. flipToast() wird aufgerufen, wenn bereits Toasts vorhanden sind. Dabei kommt die Technik FLIP von Paul Lewis zum Einsatz. Die Idee ist, die Positionsdifferenz des Behälters vor und nach dem Hinzufügen des neuen Toasts zu berechnen. Sie können sich vorstellen, wo sich der Toaster gerade befindet und wo er sein wird, und dann animieren, wo er sich befindet und wo er sich befindet.

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 das Anheben des Layouts. Wird ein neuer Toast hinzugefügt, wird er an den Anfang gestellt und mit den anderen angeordnet. Währenddessen wird eine Webanimation verwendet, um den Container von der alten Position aus zu animieren.

Der gesamte JavaScript-Code

Wenn Toast('my first toast') aufgerufen wird, ein Toast erstellt und der Seite hinzugefügt wird (vielleicht wird sogar der Container animiert, um den neuen Toast aufzunehmen), wird ein Promise zurückgegeben und der erstellte Toast für die Promise-Auflösung der CSS-Animation (die drei Keyframe-Animationen) gesehen.

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 hatte den Eindruck, dass der verwirrende Teil dieses Codes in der Promise.allSettled()-Funktion und der toast.getAnimations()-Zuordnung liegt. Da ich mehrere Keyframe-Animationen für den Toast verwendet habe, muss jede davon von JavaScript angefordert und jedes finished-Versprechen für den Abschluss eingehalten werden. Nur so kann ich sicher sein, dass alle fertig sind. allSettled funktioniert für uns. Sobald alle Versprechen erfüllt sind, gilt das als abgeschlossen. Die Verwendung von await Promise.allSettled() bedeutet, dass die nächste Codezeile das Element bedenkenlos entfernen kann und davon ausgeht, dass der Toast seinen Lebenszyklus abgeschlossen hat. Schließlich erfüllt das Aufrufen von resolve() das Toast-Versprechen, das Entwickler aufräumen können, sobald der Toast angezeigt wurde.

export default Toast

Zuletzt wird die Funktion Toast aus dem Modul exportiert, damit andere Skripts importiert und verwendet werden können.

Toast-Komponente verwenden

Wenn Sie den Toast bzw. die Entwicklungsumgebung des Toasts verwenden, müssen Sie die Funktion Toast importieren und mit einem Nachrichtenstring aufrufen.

import Toast from './toast.js'

Toast('Wizard Rose added to cart')

Wenn der Entwickler nach dem Anzeigen des Toasts Bereinigungsarbeiten oder Ähnliches vornehmen möchte, kann er „async“ verwenden und await verwenden.

import Toast from './toast.js'

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

Fazit

Jetzt weißt du, wie ich es gemacht habe. Wie würdest du es erreichen? 🙂

Diversifizieren wir unsere Ansätze und lernen Sie alle Möglichkeiten kennen, wie wir das Web nutzen können. Erstelle eine Demo und twittere mich über Links, und ich füge sie unten zum Abschnitt über Community-Remixe hinzu.

Community-Remixe