x-data
Alpine
Alpine.js · Toast · Store · Zugänglichkeit · Hyvä
Alpine.js Toast Notifications
Globales System mit Alpine.store

Toast-Benachrichtigungen sind ein Standard-UI-Pattern, das in jedem Projekt auftaucht. Mit Alpine.store lässt sich ein globales Toast-System bauen, das von jeder Komponente ausgelöst werden kann – mit Auto-Dismiss, Animationen, Queuing und vollständiger Barrierefreiheit, ohne eine externe Bibliothek zu laden.

13 Min. Lesezeit Toast · Alpine.store · Auto-Dismiss · x-transition · ARIA Alpine.js 3.x · Hyvä · Tailwind CSS v4

1. Anforderungen an ein Toast-Notification-System

Ein Toast-Notification-System muss mehrere Anforderungen gleichzeitig erfüllen: Es muss global sein – jede Komponente auf der Seite soll Benachrichtigungen auslösen können, ohne direkt mit dem Renderer zu kommunizieren. Es muss mehrere Typen unterstützen: Erfolg, Fehler, Warnung und Information. Jede Benachrichtigung soll nach einer konfigurierbaren Zeit automatisch verschwinden, aber Benutzer sollen sie durch Hovern oder Fokussieren anhalten können. Mehrere Benachrichtigungen sollen gleichzeitig sichtbar sein, aber die maximale Anzahl soll konfigurierbar sein, um die UI nicht zu überladen. Und das alles muss zugänglich sein: Screenreader sollen Benachrichtigungen ankündigen, Tastaturbenutzern soll es möglich sein, sie zu schließen.

Die Architektur für dieses System in Alpine.js ist klar: Ein zentraler Alpine.store verwaltet die Liste der aktiven Benachrichtigungen und stellt Actions bereit, um neue hinzuzufügen oder vorhandene zu entfernen. Eine Renderer-Komponente mit x-data liest diesen Store und rendert die Toasts mit x-for. Sie wird einmal in der Seite platziert – typischerweise direkt vor dem schließenden </body>-Tag oder in einem Layout-Template. Jede andere Komponente auf der Seite kann dann einfach $store.notifications.add(...) aufrufen und der Renderer kümmert sich um den Rest.

2. Der Notification-Store: Datenstruktur und Actions

Der Store ist das Herzstück des Systems. Er enthält die Liste der aktiven Benachrichtigungen als Array von Objekten. Jedes Objekt hat eine eindeutige ID (für die :key-Bindung in x-for), den Typ, die Nachricht, eine optionale Überschrift, die Dauer bis zum Auto-Dismiss und einen Timer-Handle für das spätere Löschen. Die Actions add() und remove() sind die einzigen öffentlichen Schnittstellen des Stores.


document.addEventListener('alpine:init', () => {
  Alpine.store('notifications', {
    items: [],
    maxVisible: 5,
    defaultDuration: 4000,
    _nextId: 1,

    /**
     * Add a notification to the stack.
     * @param {string} message
     * @param {'success'|'error'|'warning'|'info'} type
     * @param {object} options
     */
    add(message, type = 'info', options = {}) {
      const id = this._nextId++;
      const duration = options.duration ?? this.defaultDuration;

      const notification = {
        id,
        message,
        type,
        title: options.title ?? null,
        duration,
        persistent: options.persistent ?? false,
        paused: false,
        _timer: null
      };

      // Enforce max visible — remove oldest if over limit
      if (this.items.length >= this.maxVisible) {
        const oldest = this.items[0];
        clearTimeout(oldest._timer);
        this.items.shift();
      }

      this.items.push(notification);

      // Auto-dismiss after duration (unless persistent)
      if (!notification.persistent && duration > 0) {
        notification._timer = setTimeout(() => {
          this.remove(id);
        }, duration);
      }

      return id;
    },

    remove(id) {
      const index = this.items.findIndex(n => n.id === id);
      if (index === -1) return;
      clearTimeout(this.items[index]._timer);
      this.items.splice(index, 1);
    },

    pauseTimer(id) {
      const n = this.items.find(n => n.id === id);
      if (n && n._timer) {
        clearTimeout(n._timer);
        n.paused = true;
      }
    },

    resumeTimer(id) {
      const n = this.items.find(n => n.id === id);
      if (n && n.paused && !n.persistent) {
        n._timer = setTimeout(() => this.remove(id), n.duration / 2);
        n.paused = false;
      }
    },

    // Shorthand helpers
    success(msg, opts = {}) { return this.add(msg, 'success', opts); },
    error(msg, opts = {})   { return this.add(msg, 'error',   { duration: 0, persistent: true, ...opts }); },
    warning(msg, opts = {}) { return this.add(msg, 'warning', opts); },
    info(msg, opts = {})    { return this.add(msg, 'info', opts); },

    clearAll() {
      this.items.forEach(n => clearTimeout(n._timer));
      this.items = [];
    }
  });
});

3. Die Renderer-Komponente: DOM und Animationen

Die Renderer-Komponente liest den Store und rendert die Toasts. Sie wird einmal in der Seite platziert und hat keine eigene Geschäftslogik – sie ist rein für die Darstellung zuständig. x-for iteriert über $store.notifications.items, x-transition sorgt für Ein- und Ausblend-Animationen. Die Position des Containers wird mit absoluten CSS-Klassen festgelegt – typischerweise unten rechts auf dem Bildschirm.


<!-- Notification Renderer — einmal am Ende des Body platzieren -->
<!-- Kein eigenes x-data nötig — Store direkt über $store zugänglich -->
<div
  x-data
  class="fixed bottom-4 right-4 z-50 flex flex-col gap-3 w-full max-w-sm"
  role="region"
  aria-label="Benachrichtigungen"
  aria-live="polite"
  aria-atomic="false"
>
  <template x-for="notification in $store.notifications.items" :key="notification.id">
    <div
      x-show="true"
      x-transition:enter="transition ease-out duration-300"
      x-transition:enter-start="opacity-0 translate-y-4 scale-95"
      x-transition:enter-end="opacity-100 translate-y-0 scale-100"
      x-transition:leave="transition ease-in duration-200"
      x-transition:leave-start="opacity-100 translate-y-0 scale-100"
      x-transition:leave-end="opacity-0 translate-y-2 scale-95"
      @mouseenter="$store.notifications.pauseTimer(notification.id)"
      @mouseleave="$store.notifications.resumeTimer(notification.id)"
      @focusin="$store.notifications.pauseTimer(notification.id)"
      @focusout="$store.notifications.resumeTimer(notification.id)"
      class="relative flex items-start gap-3 p-4 rounded-xl shadow-xl border"
      :class="{
        'bg-emerald-50 border-emerald-200 text-emerald-900': notification.type === 'success',
        'bg-red-50 border-red-200 text-red-900': notification.type === 'error',
        'bg-amber-50 border-amber-200 text-amber-900': notification.type === 'warning',
        'bg-blue-50 border-blue-200 text-blue-900': notification.type === 'info'
      }"
      role="alert"
      :aria-label="notification.type + ': ' + notification.message"
    >
      <!-- Icon-Bereich -->
      <div class="flex-shrink-0 mt-0.5">
        <!-- Success-Icon, Error-Icon, etc. via :class -->
      </div>

      <!-- Inhalt -->
      <div class="flex-1 min-w-0">
        <p x-show="notification.title" x-text="notification.title"
           class="font-semibold text-sm mb-0.5"></p>
        <p x-text="notification.message" class="text-sm"></p>
      </div>

      <!-- Schließen-Button -->
      <button
        @click="$store.notifications.remove(notification.id)"
        class="flex-shrink-0 opacity-50 hover:opacity-100 transition-opacity"
        :aria-label="'Benachrichtigung schließen: ' + notification.message"
      >
        <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
          <path d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"/>
        </svg>
      </button>
    </div>
  </template>
</div>

4. Notification-Typen: Success, Error, Warning, Info

Vier Standard-Typen decken den Großteil aller Anwendungsfälle ab. Success-Toasts bestätigen erfolgreiche Aktionen: Produkt zum Warenkorb hinzugefügt, Formular abgesendet, Einstellungen gespeichert. Sie verschwinden nach kurzer Zeit automatisch. Error-Toasts zeigen kritische Fehler an und bleiben standardmäßig persistent – der Benutzer muss sie aktiv schließen, weil die Fehlermeldung relevant bleibt. Warning-Toasts informieren über nicht-kritische Probleme, die Aufmerksamkeit verdienen, aber keine sofortige Aktion erfordern. Info-Toasts liefern neutrale Informationen.

Die Unterscheidung der Typen erfolgt ausschließlich im Store – die Renderer-Komponente reagiert auf den type-Wert mit den entsprechenden CSS-Klassen. Das macht es einfach, das visuelle Design zu ändern, ohne die Store-Logik zu berühren. Jeder Typ kann über eine eigene Shorthand-Methode ausgelöst werden: $store.notifications.success('Warenkorb aktualisiert'), $store.notifications.error('Server nicht erreichbar').

5. Auto-Dismiss und Pause-on-Hover

Auto-Dismiss ist mit setTimeout implementiert, dessen Handle im Notification-Objekt gespeichert wird. Das ermöglicht, den Timer bei Bedarf zu stoppen – zum Beispiel wenn der Benutzer mit der Maus über den Toast fährt. Die pauseTimer()-Methode räumt den Timer auf, resumeTimer() setzt einen neuen Timer mit der halben ursprünglichen Dauer (da der Benutzer den Toast bereits gelesen hat). Dieses Pattern respektiert die Benutzerinteraktion und verhindert, dass Benachrichtigungen verschwinden, während der Benutzer sie liest.

Error-Benachrichtigungen sind standardmäßig persistent (duration: 0, persistent: true), weil Fehlermeldungen oft Kontext für eine notwendige Aktion liefern. Das Muster kann pro Aufruf überschrieben werden: $store.notifications.error('Fehler', { persistent: false, duration: 5000 }). Die clearAll()-Methode räumt alle Timer auf, bevor sie das Array leert, um Speicherlecks durch nicht abgebrochene Timer zu vermeiden.

6. Queuing und maximale Anzahl gleichzeitiger Toasts

Wenn mehrere Aktionen schnell hintereinander Benachrichtigungen auslösen – etwa beim Hinzufügen mehrerer Produkte zum Warenkorb – kann die Toast-Stapel die UI überfluten. Die maxVisible-Eigenschaft begrenzt die Anzahl gleichzeitiger Toasts. Überschreitet die neue Benachrichtigung das Limit, wird die älteste entfernt. Dieses Verhalten ist bewusst nicht als echtes Queue-System implementiert (wo überschüssige Toasts warten würden), weil veraltete Benachrichtigungen selten nützlich sind, wenn der Benutzer sie nicht sofort sieht.


// Verwendung aus beliebigen Komponenten — ohne direkten Komponent-Zugriff
// Einfache Verwendung im HTML-Template
// <button @click="$store.notifications.success('Gespeichert!')">Speichern</button>

// Verwendung in einer async Funktion
document.addEventListener('alpine:init', () => {
  Alpine.data('checkoutForm', () => ({
    submitting: false,
    formData: { name: '', email: '', address: '' },

    async submit() {
      this.submitting = true;
      try {
        const res = await fetch('/api/checkout', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify(this.formData)
        });

        if (!res.ok) {
          const err = await res.json();
          // Persistenter Fehler-Toast — bleibt bis manuell geschlossen
          Alpine.store('notifications').error(
            err.message || 'Bestellung fehlgeschlagen. Bitte erneut versuchen.',
            { title: 'Fehler beim Absenden' }
          );
          return;
        }

        const data = await res.json();
        // Erfolgs-Toast mit kurzer Dauer
        Alpine.store('notifications').success(
          `Bestellung #${data.orderId} erfolgreich abgeschlossen!`,
          { title: 'Bestellung bestätigt', duration: 6000 }
        );

        // Formular zurücksetzen
        this.formData = { name: '', email: '', address: '' };

      } catch (networkError) {
        Alpine.store('notifications').error(
          'Netzwerkfehler – bitte Internetverbindung prüfen.',
          { title: 'Verbindungsproblem', persistent: true }
        );
      } finally {
        this.submitting = false;
      }
    }
  }));
});

7. Barrierefreiheit: ARIA-Live-Regions und Tastatur

Toast-Notifications sind aus Barrierefreiheitsperspektive herausfordernd: Sie erscheinen außerhalb des aktuellen Fokus-Kontexts und müssen von Screenreadern angekündigt werden, ohne den Benutzer zu unterbrechen. Die Lösung ist eine aria-live="polite"-Region, die neue Inhalte ankündigt, sobald der Benutzer eine Pause macht. Das role="alert" auf jedem Toast macht ihn für Screenreader als wichtige Benachrichtigung erkennbar. Der Schließen-Button muss ein beschreibendes aria-label haben, das die Benachrichtigungsmessage enthält, damit Benutzer wissen, was sie schließen.

Tastaturnavigation bedeutet für Toasts primär: Der Schließen-Button muss per Tab erreichbar sein. Da Toasts nicht im normalen Dokumentenfluss stehen, muss der Container dennoch im Fokus-Fluss erreichbar sein. Das role="region" auf dem Container mit einem aria-label ermöglicht Screenreader-Benutzern, direkt zur Benachrichtigungsregion zu springen. Das Pausieren der Timer bei @focusin stellt sicher, dass Benachrichtigungen nicht verschwinden, während der Benutzer den Schließen-Button fokussiert.

8. Integration in Hyvä und Magento 2

In Hyvä-Projekten ersetzt ein solches Toast-System die eingebauten Hyvä-Meldungen oder ergänzt sie. Der Store wird in einem .phtml-Template im <head> registriert. Die Renderer-Komponente wird in ein eigenes Template ausgelagert, das über Layout-XML am Ende des Body eingebunden wird. Bestehende Hyvä-Komponenten, die intern Messages emittieren, können über das Custom-Event-System mit dem Store kommunizieren: Sie feuern ein CustomEvent, und der Store hört auf dieses Event.

Für Magento-2-spezifische Nachrichten – wie Warenkorb-Updates oder Formularfehler – lässt sich der Store auch über PHP-generierte Inline-Scripts ansteuern. Das Muster ist: PHP rendert die Nachrichtendaten als JSON, das Inline-Script liest die Daten und ruft die Store-Action auf. Nach dem Inline-Script-Block folgt immer <?= $hyvaCsp->registerInlineScript() ?>.

9. Vergleich: Eigenes System vs. Bibliotheken

Bibliotheken wie Toastify.js, Notyf oder SweetAlert2 bieten fertige Toast-Systeme. Der Vorteil des eigenen Systems mit Alpine.store ist die vollständige Integration in Alpine.js-Reaktivität ohne zusätzliche Abhängigkeiten und Bundle-Größe, die direkte Kontrolle über Styling mit Tailwind CSS und die Kompatibilität mit der CSP-Policy von Magento 2.

Aspekt Alpine.store (eigen) Toastify.js SweetAlert2
Bundle-Größe 0 KB zusätzlich ~5 KB ~47 KB
Tailwind-Integration Nativ CSS-Override nötig CSS-Override komplex
Alpine-Reaktivität Vollständig Adapter nötig Adapter nötig
CSP-kompatibel Ja (inline script) Meist ja Erfordert unsafe-eval
Wartungsaufwand Eigener Code Bibliotheks-Updates Bibliotheks-Updates

10. Zusammenfassung

Ein globales Toast-Notification-System mit Alpine.store deckt alle typischen Anforderungen ohne externe Bibliothek: Auto-Dismiss mit Pause-on-Hover, vier Benachrichtigungstypen, konfigurierbare Persistenz, Queuing über maxVisible und vollständige Barrierefreiheit mit ARIA-Live-Regions. Die Architektur trennt sauber: Der Store enthält die Logik und den State, die Renderer-Komponente ist rein für die Darstellung zuständig, und jede andere Komponente kann Benachrichtigungen auslösen, ohne den Store direkt zu importieren.

In Hyvä-Projekten ist dieses Pattern besonders wertvoll, weil es sich nahtlos in die bestehende Alpine.js-Infrastruktur integriert und CSP-konform ist. Die Shorthand-Methoden success(), error(), warning() und info() machen die Verwendung minimal aufwändig – eine Zeile Code reicht, um eine aussagekräftige Benachrichtigung mit Titel, Typ und Dauer auszulösen.

Toast Notifications mit Alpine.store — Das Wichtigste auf einen Blick

Store-Struktur

Array von Notification-Objekten mit ID, Typ, Nachricht, Timer-Handle. Actions: add, remove, pauseTimer, resumeTimer, clearAll.

Auto-Dismiss

setTimeout-Handle im Objekt speichern. Pause on hover/focus mit clearTimeout/setTimeout. Errors standardmäßig persistent.

Barrierefreiheit

Container: aria-live="polite". Jeder Toast: role="alert". Schließen-Button: aria-label mit Nachrichtentext. Timer pausieren bei focus.

Hyvä-Integration

Store in alpine:init registrieren. Renderer als eigenes Template am Ende des Body. $hyvaCsp->registerInlineScript() nach jedem Script.

Mironsoft

Alpine.js, Hyvä-Themes und Magento-2-Frontend-Entwicklung

UI-Komponenten für euer Hyvä-Projekt?

Wir bauen zugängliche, performante UI-Komponenten mit Alpine.js und Tailwind CSS – Toast-Systeme, Modals, Dropdowns und mehr, CSP-konform und ohne externe Abhängigkeiten.

UI-Komponenten

Toasts, Modals, Dropdowns und Formulare zugänglich und CSP-konform

Hyvä-Integration

Nahtlose Integration in Hyvä-Layouts mit Layout-XML und phtml-Templates

Barrierefreiheit

WCAG 2.1 AA-konforme Komponenten mit ARIA und Tastaturnavigation

11. FAQ: Alpine.js Toast Notifications

1Toast aus beliebiger Komponente auslösen?
Template: $store.notifications.success('...'). JavaScript: Alpine.store('notifications').error('...'). Kein Import nötig.
2Warum sind Errors standardmäßig persistent?
Fehlermeldungen brauchen Zeit zum Lesen. Auto-Dismiss würde wichtige Kontext-Information entfernen. Überschreibbar mit { persistent: false, duration: 5000 }.
3Wie viele Toasts gleichzeitig?
maxVisible im Store konfigurieren (Standard: 5). Bei Überschreitung wird der älteste Toast entfernt.
4Wie funktioniert Pause-on-Hover?
@mouseenter: clearTimeout (Timer stoppen). @mouseleave: neuer Timer mit halber Dauer. Auch für @focusin/@focusout.
5Zugänglich für Screenreader?
Container: aria-live="polite". Toast: role="alert". Schließen-Button: aria-label mit Nachrichtentext. Timer pausieren bei Focus.
6Eigene Toast-Typen hinzufügen?
Beliebigen type-String in add() übergeben. Im Renderer die passende CSS-Klasse für den neuen Typ ergänzen.
7Timer-Leaks verhindern?
Handle im Objekt speichern. Beim Entfernen immer clearTimeout(n._timer). clearAll() alle Timer stoppen, bevor Array geleert wird.
8Hyvä-Integration?
Store im alpine:init in .phtml. $hyvaCsp->registerInlineScript() nach Script. Renderer am Ende des Body per Layout-XML.
9Toast mit Aktion-Button?
Notification-Objekt um action: { label, callback } erweitern. Renderer rendert Button und ruft Callback bei Klick auf.
10Renderer-Position in Hyvä?
Vor dem schließenden </body>-Tag in eigenem .phtml-Template, eingebunden über default.xml am Body-Ende.