x-data
Alpine
Alpine.js · Transitions · CSS Animationen · Performance
Alpine.js Transitions:
Animationen die wirklich smooth sind

Ruckelnde Animationen, Inhalt der beim Öffnen sofort erscheint oder beim Schließen abrupt verschwindet – das sind die Symptome eines falsch eingesetzten Transitions-Systems. Alpine.js bietet mit x-transition ein durchdachtes Framework für flüssige Enter- und Leave-Animationen, das mit wenig Code professionelle Ergebnisse liefert.

16 Min. Lesezeit x-transition · Enter · Leave · CSS · Koordination Alpine.js 3.x · Tailwind CSS v4 · prefers-reduced-motion

1. Wie x-transition funktioniert: Enter/Leave-Phasen

Das x-transition Direktive in Alpine.js basiert auf einem dreiphasigen System für jede Richtung. Beim Einblenden (Enter) gibt es die Phase x-transition:enter für Styles während der gesamten Enter-Transition, x-transition:enter-start für den Startzustand und x-transition:enter-end für den Endzustand. Analog dazu laufen beim Ausblenden (Leave) x-transition:leave, x-transition:leave-start und x-transition:leave-end. Alpine.js managed die Klassen automatisch: enter-start wird gesetzt, dann nach einem Tick auf enter-end gewechselt, und das triggert die CSS-Transition.

Dieses System ist bewusst so aufgebaut, dass CSS die eigentliche Animation übernimmt. Alpine.js setzt nur die Klassen, der Browser führt die Transition aus. Das bedeutet: x-transition nutzt den GPU-beschleunigten CSS-Transition-Mechanismus des Browsers – keine JavaScript-Animationsschleife, kein requestAnimationFrame-Overhead für die eigentliche Bewegung. Alpine.js managt nur den Lebenszyklus: Wann wird das Element ins DOM eingefügt, wann werden Klassen gesetzt, wann wird es entfernt.

Ein häufiges Missverständnis: x-transition funktioniert nur in Kombination mit x-show oder x-if. Es ist kein eigenständiger Trigger, sondern ein Modifier, der auf die Sichtbarkeitsänderung reagiert. x-show ist dabei die häufigere Wahl für Transitions, weil x-if das Element komplett aus dem DOM entfernt und bei der nächsten Enter-Phase neu einfügt – was für komplexe Transitions zu einem kurzen Layout-Recalc führen kann.

2. x-transition Modifier: duration, delay, opacity, scale

Alpine.js bietet für einfache Fälle praktische Inline-Modifier direkt am x-transition-Direktiv. Mit x-transition.duration.300ms wird die Transition-Dauer auf 300ms gesetzt. Mit x-transition.opacity wird nur die Opacity animiert. Mit x-transition.scale.90 startet das Element bei 90% Größe. Diese Modifier lassen sich kombinieren: x-transition.duration.200ms.opacity.scale.95 erzeugt eine Fade-in-Shrink-Animation in 200ms.

Die Modifier sind praktisch für einfache Anwendungsfälle, haben aber klare Grenzen. Sie unterstützen keine unterschiedlichen Enter- und Leave-Dauern, keine verzögerten Starts (transition-delay) und keine komplexen Easing-Funktionen jenseits der Browser-Defaults. Sobald die Animation komplexer wird – unterschiedliche Easing-Kurven, Sequenzen, oder spezifische Transform-Eigenschaften – ist der Wechsel auf Custom-CSS-Klassen der richtige Schritt.

// Einfache Modifier-Variante für schnelle Dropdown-Transitions
// Im Template: 
// Fortgeschrittene Variante: Separate Enter/Leave-Phasen mit Custom-Klassen // Im Template: //
// Alpine.js Komponente für ein animiertes Dropdown function animatedDropdown() { return { open: false, selectedLabel: 'Auswählen...', options: [ { value: 'de', label: 'Deutschland' }, { value: 'at', label: 'Österreich' }, { value: 'ch', label: 'Schweiz' } ], toggle() { this.open = !this.open; }, select(option) { this.selectedLabel = option.label; this.open = false; this.$dispatch('vendor:option-selected', { value: option.value }); }, close() { this.open = false; } }; }

3. Custom CSS-Klassen statt Inline-Modifier

Für professionelle Animationen sind Custom CSS-Klassen in den sechs x-transition-Phasen die richtige Wahl. Das System erlaubt maximale Kontrolle: Unterschiedliche transition-timing-function für Enter (ease-out für natürliches Einblenden) und Leave (ease-in für natürliches Ausblenden), spezifische Transform-Eigenschaften, Delays für gestaffelte Animationen und die Nutzung beliebiger CSS-Animationen. In Hyvä mit Tailwind CSS v4 stehen alle Transition-Utilities direkt bereit.

Das wichtige Muster bei Custom-CSS-Transitions: Die transition-Property gehört in die x-transition:enter und x-transition:leave Klassen – also in die Basis-Klassen, nicht in die Start/End-Klassen. enter-start und enter-end definieren nur die CSS-Werte, zwischen denen der Browser interpoliert. Ein häufiger Fehler: Die transition-Property in enter-start zu setzen, was dazu führt, dass der Browser sie zu spät liest und keine Animation stattfindet.

// CSS für smooth Sidebar-Animation (in Tailwind CSS v4 oder Custom CSS)
// .sidebar-enter: transition-property: transform, opacity; transition-duration: 350ms; transition-timing-function: cubic-bezier(0.16, 1, 0.3, 1);
// .sidebar-enter-start: transform: translateX(-100%); opacity: 0;
// .sidebar-enter-end: transform: translateX(0); opacity: 1;
// .sidebar-leave: transition-property: transform, opacity; transition-duration: 250ms; transition-timing-function: cubic-bezier(0.7, 0, 0.84, 0);
// .sidebar-leave-start: transform: translateX(0); opacity: 1;
// .sidebar-leave-end: transform: translateX(-100%); opacity: 0;

function mobileSidebar() {
  return {
    open: false,

    init() {
      // ESC-Taste schließt Sidebar
      this.$el.addEventListener('keydown', (e) => {
        if (e.key === 'Escape' && this.open) this.close();
      });

      // Body-Scroll sperren wenn Sidebar offen
      this.$watch('open', (isOpen) => {
        document.body.style.overflow = isOpen ? 'hidden' : '';
      });
    },

    toggle() { this.open = !this.open; },

    close() {
      this.open = false;
      // Fokus zurück zum Trigger
      this.$el.querySelector('[data-sidebar-trigger]')?.focus();
    }
  };
}

4. Koordinierte Animationen und Sequenzen

Koordinierte Animationen entstehen, wenn mehrere Elemente nacheinander oder parallel animiert werden – wie eine Overlay-Backdrop die zuerst einfadet, und dann ein Modal-Dialog der leicht verzögert einfährt. In Alpine.js löst man das mit transition-delay in den CSS-Klassen der einzelnen Elemente, während der übergeordnete Toggle-Zustand gemeinsam ist. Beide Elemente reagieren auf dieselbe x-show-Bedingung, haben aber unterschiedliche Delay-Werte.

Für aufwändigere Sequenzen – etwa eine Liste von Items, die nacheinander einblenden – nutzt man JavaScript-basiertes Delay in Kombination mit Alpine.js-State. Jedes Item bekommt einen Index, aus dem ein transition-delay berechnet wird: Item 0 verzögert 0ms, Item 1 verzögert 50ms, Item 2 verzögert 100ms. Das erzeugt einen Stagger-Effekt, der deutlich professioneller wirkt als alle Items gleichzeitig einzublenden.

// Koordinierte Modal-Animation: Backdrop + Dialog mit Delay
function coordinatedModal() {
  return {
    open: false,
    // Backdrop und Dialog reagieren auf denselben State
    // Im Template: Backdrop mit Delay 0, Dialog mit Delay 75ms

    show() {
      this.open = true;
      // Fokus ins Modal nach Transition
      this.$nextTick(() => {
        this.$el.querySelector('[data-modal-first-focus]')?.focus();
      });
    },

    hide() {
      this.open = false;
    }
  };
}

// Stagger-Animation für Produktkarten-Liste
function productGrid() {
  return {
    visible: false,
    items: [],

    init() {
      // Items mit gestaffeltem Delay markieren
      this.$nextTick(() => {
        const cards = this.$el.querySelectorAll('[data-product-card]');
        cards.forEach((card, i) => {
          card.style.transitionDelay = `${i * 60}ms`;
        });
        // Nach kurzem Delay sichtbar schalten (verhindert FOUC)
        requestAnimationFrame(() => { this.visible = true; });
      });
    }
  };
}

Ein korrektes Modal-Pattern mit Alpine.js-Transitions besteht aus drei Teilen: einem Backdrop-Element, dem Dialog-Element und dem Scroll-Lock auf dem Body. Alle drei müssen koordiniert animiert werden. Der Backdrop faded mit einer einfachen Opacity-Transition ein und aus. Der Dialog kombiniert Opacity mit einer vertikalen Translate-Animation (leicht nach unten beim Einblenden, zurück beim Ausblenden), die dem natürlichen Schwerkraft-Gefühl entspricht. Der Body-Scroll-Lock wird synchron mit dem State-Toggle gesetzt und entfernt.

Was in vielen Implementierungen fehlt: das Fokus-Management. Beim Öffnen muss der Fokus ins Modal, beim Schließen muss er zurück zum auslösenden Element. Alpine.js $nextTick stellt sicher, dass die DOM-Änderung abgeschlossen ist, bevor der Fokus gesetzt wird. Ohne korrekte Fokus-Verwaltung ist das Modal für Tastatur-Nutzer und Screen-Reader nicht nutzbar – und in Hyvä-Projekten, die in einem professionellen Kontext eingesetzt werden, ist das ein Pflichtkriterium.

6. Listen-Transitionen: Items animiert ein- und ausblenden

Das Animieren von Listen-Items in Alpine.js ist konzeptuell anders als in Frameworks wie Vue, die dedizierte Transition-Group-Komponenten mitbringen. In Alpine.js setzt man x-show mit x-transition auf jedem einzelnen List-Item und steuert Sichtbarkeit über den Item-State. Für dynamisch hinzugefügte Items – etwa in einer gefilterten Produktliste – kombiniert man x-for mit x-transition direkt am Loop-Element.

Ein kritisches Detail bei Listen-Transitions: x-for und x-transition funktionieren zusammen, aber die Transition betrifft das gesamte Loop-Element. Wenn Items entfernt werden, läuft die Leave-Transition, bevor das Element aus dem DOM entfernt wird. Das ist das korrekte Verhalten – aber es bedeutet, dass der Container während der Leave-Transition noch Platz für die verschwindenden Items reserviert. Wer das nicht berücksichtigt, sieht Layout-Sprünge beim Entfernen von gefilterten Items.

// Animierte gefilterte Liste mit Alpine.js
function filteredList() {
  return {
    search: '',
    allItems: [
      { id: 1, name: 'Alpine.js Einführung', tag: 'tutorial' },
      { id: 2, name: 'Tailwind CSS Grid', tag: 'css' },
      { id: 3, name: 'Hyvä Performance', tag: 'performance' },
      { id: 4, name: 'x-transition Guide', tag: 'tutorial' },
      { id: 5, name: 'Custom Directives', tag: 'advanced' }
    ],

    get filtered() {
      if (!this.search) return this.allItems;
      const q = this.search.toLowerCase();
      return this.allItems.filter(item =>
        item.name.toLowerCase().includes(q) ||
        item.tag.toLowerCase().includes(q)
      );
    },

    // Key für x-for: verhindert unnötiges DOM-Recycling
    // Im Template: