x-data
Alpine
Alpine.js · Carousel · Touch · Tastaturnavigation · ARIA
Alpine.js Carousel Slider
Touch-Support und Keyboard-Navigation

Swiper.js und Slick kommen mit Hunderten von Kilobytes und eigenen JavaScript-Ökosystemen. Ein produktionsreifer Carousel-Slider für Hyvä braucht weder das eine noch das andere – Alpine.js reicht aus für Touch, Keyboard, Auto-Play und ARIA.

14 Min. Lesezeit Touch Events · x-data · @keydown · ARIA · Auto-Play Alpine.js 3.x · Tailwind CSS · Hyvä

1. Warum kein Swiper.js auf Hyvä-Seiten

Swiper.js ist die populärste Carousel-Bibliothek im Web – und mit 140 KB minified (ohne gzip) und einem eigenen Modul-System auch die schwerste. Auf einer Hyvä-Seite, die ohne jQuery und ohne Knockout.js auskommt und deren gesamtes JavaScript-Budget für den kritischen Pfad unter 50 KB liegen soll, ist Swiper.js eine inakzeptable Abhängigkeit. Hinzu kommt: Swiper.js bringt sein eigenes Event-System mit, das mit Alpine.js' reaktivem Proxy-System interferiert, wenn beide auf denselben DOM-Elementen arbeiten.

Das eigentliche Kernproblem ist aber ein anderes: Die meisten Produktseiten auf Magento-Shops brauchen keinen Feature-reichhaltigen Slider mit virtueller DOM-Abstraktion. Sie brauchen: Vorwärts/Rückwärts navigieren, Touch-Swipe auf Mobilgeräten, Tastaturnavigation für Barrierefreiheit und optional Auto-Play. All das lässt sich mit Alpine.js in unter 120 Zeilen realisieren. Der Slider in diesem Tutorial nutzt natives CSS für Transitionen, Pointer Events für Touch-Swipe und Alpine-Direktiven für den State. Das Ergebnis ist ein Slider ohne externe Abhängigkeit, der auf allen Geräten funktioniert und keine zusätzlichen Netzwerkanfragen verursacht.

2. Carousel-State mit Alpine.js aufbauen

Der Kern des Carousels ist ein einfaches State-Objekt mit einem einzigen reaktiven Wert: dem aktuellen Index. Alle anderen Zustände – ob ein Vorwärts- oder Rückwärts-Button disabled ist, welcher Paginierungspunkt aktiv ist, ob die Live-Region den neuen Slide ankündigen soll – leiten sich aus diesem Index ab. Das ist das zentrale Alpine-Muster: Einen minimalen State halten, aus dem alle UI-Zustände berechnet werden. Kein Doppel-State, der manuell synchronisiert werden muss.

Die Slide-Elemente selbst werden nicht von Alpine verwaltet – sie sind statisches HTML. Alpine kennt nur den Index und blendet Slides mit :class="{ hidden: currentIndex !== index }" ein und aus. Das ist absichtlich einfach gehalten: Keine virtuellen DOM-Nodes, kein x-for, das bei Scroll-Positionen oder Transition-Timings zu Race Conditions führen kann. Die Transition wird rein über CSS transition-opacity und transition-transform gesteuert, was der Browser nativ und performant auf dem Compositor-Thread abarbeitet.


// Alpine.js Carousel — minimal reactive state
function carousel(totalSlides, options = {}) {
  return {
    current: 0,
    total: totalSlides,
    autoPlayInterval: null,
    paused: false,

    get canPrev() { return this.current > 0; },
    get canNext() { return this.current < this.total - 1; },

    prev() {
      this.current = this.current > 0
        ? this.current - 1
        : options.loop ? this.total - 1 : 0;
      this.announce();
    },
    next() {
      this.current = this.current < this.total - 1
        ? this.current + 1
        : options.loop ? 0 : this.total - 1;
      this.announce();
    },
    goTo(index) {
      this.current = Math.max(0, Math.min(index, this.total - 1));
      this.announce();
    },
    announce() {
      // Updates aria-live region — Alpine reacts to current change automatically
      this.$nextTick(() => {
        this.$refs.liveRegion.textContent =
          `Slide ${this.current + 1} von ${this.total}`;
      });
    }
  };
}

3. CSS-Transition statt JavaScript-Animation

JavaScript-basierte Animationen mit requestAnimationFrame oder setInterval laufen auf dem Hauptthread und können durch JavaScript-Arbeit gestört werden. CSS-Transitionen und CSS-Animationen hingegen werden – sofern sie nur opacity und transform animieren – auf dem Compositor-Thread ausgeführt und bleiben auch unter JavaScript-Last flüssig. Für einen Carousel-Slider sind das genau die zwei Eigenschaften, die animiert werden müssen: Opacity für Fade-Übergänge, Transform für Slide-Übergänge.

Die Implementierung ist einfach: Alle Slides sind absolut positioniert im Container. Jeder Slide hat transition: opacity 0.4s ease, transform 0.4s ease. Der aktive Slide ist opacity-100 translate-x-0, der vorherige ist opacity-0 -translate-x-full, der nächste ist opacity-0 translate-x-full. Alpine setzt über :class die passenden Tailwind-Klassen basierend auf dem Verhältnis zwischen current und index. Keine JavaScript-Animation, kein GSAP, kein requestAnimationFrame – der Browser macht alles.

4. Touch-Swipe mit Pointer Events erkennen

Pointer Events sind der moderne Ersatz für Touch Events und Mouse Events: Ein einzelner Event-Handler funktioniert für Maus, Touch und Stylus. Das macht den Swipe-Code deutlich schlanker als eine Implementierung mit separaten touchstart, touchmove, touchend-Handlern. Die Swipe-Logik ist linear: Bei pointerdown wird der X-Koordinate gespeichert, bei pointerup wird die Differenz berechnet. Ist sie größer als ein Schwellenwert (typisch 50px), wird zum nächsten oder vorherigen Slide gewechselt.

Wichtig: setPointerCapture auf dem Element beim pointerdown-Event stellt sicher, dass alle folgenden Pointer Events auf diesem Element landen – auch wenn der Nutzer den Finger über die Grenzen des Elements hinaus bewegt. Ohne Pointer Capture verliert man das pointermove-Event sobald der Finger das Element verlässt, was bei schnellen Swipes zu Abbrüchen führt. Das touch-action: pan-y CSS-Property am Container erlaubt normales vertikales Scrollen, verhindert aber Browser-Interventionen beim horizontalen Wischen.


// Touch/Pointer swipe detection — works for mouse, touch, and stylus
initSwipe() {
  let startX = 0;
  const THRESHOLD = 50; // px — minimum swipe distance

  this.$refs.track.addEventListener('pointerdown', e => {
    startX = e.clientX;
    this.$refs.track.setPointerCapture(e.pointerId);
  });

  this.$refs.track.addEventListener('pointerup', e => {
    const deltaX = e.clientX - startX;
    if (Math.abs(deltaX) < THRESHOLD) return;
    deltaX < 0 ? this.next() : this.prev();
  });

  // Prevent click events from firing after a swipe
  this.$refs.track.addEventListener('click', e => {
    if (Math.abs(e.clientX - startX) > THRESHOLD) e.stopPropagation();
  });
},

5. Tastaturnavigation nach ARIA-Roving-Tabindex

Für Carousels definiert die ARIA Authoring Practices das Roving-Tabindex-Pattern: Nur das aktive Slide-Steuerelement hat tabindex="0", alle anderen haben tabindex="-1". Wenn der Nutzer mit der Tastatur zu einem anderen Slide wechselt, erhält der neue Button tabindex="0" und wird programmatisch fokussiert, der alte bekommt tabindex="-1". Das verhindert, dass Tastaturnutzer durch alle Paginierungspunkte tabben müssen – sie navigieren mit Arrow-Tasten innerhalb des Carousel-Widgets.

Zusätzlich müssen die Navigations-Buttons (Vorherige, Nächste) über die Tastatur erreichbar sein. Die eigentlichen Slide-Inhalte sollten nur für den aktiven Slide in der Fokus-Reihenfolge liegen – inactive Slides bekommen inert-Attribut oder aria-hidden="true", damit Tab nicht durch versteckte Links im inaktiven Slide führt. Das inert-Attribut ist moderne Lösung: Es entfernt alle Elemente innerhalb eines Containers aus der Fokus-Reihenfolge und aus dem Accessibility Tree gleichzeitig.

6. Auto-Play mit Pause-on-Hover und Pause-on-Focus

Auto-Play in Carousels ist nach WCAG 2.1 Erfolgskriterium 2.2.2 nur erlaubt, wenn der Nutzer es pausieren, stoppen oder verlangsamen kann. Die Mindestanforderung ist eine sichtbare Pause-Schaltfläche. Darüber hinaus empfiehlt die WCAG-Technik G4, das Auto-Play automatisch zu pausieren, wenn der Mauszeiger über dem Carousel ist (Hover) und wenn ein Element innerhalb des Carousels fokussiert ist (Focus). Beides verhindert, dass ein Slide wechselt, während der Nutzer gerade versucht, einen Link im aktuellen Slide zu aktivieren.

Die Implementierung nutzt Alpine's @mouseenter, @mouseleave, @focusin und @focusout am Container. Der @focusout.capture prüft, ob der neue Fokus noch innerhalb des Containers liegt (this.$el.contains(e.relatedTarget)), bevor das Auto-Play wieder gestartet wird. Das setInterval des Auto-Play wird in init() gestartet und in destroy() via clearInterval aufgeräumt. Alpine ruft init() beim Mount und destroy() beim Unmount automatisch auf – kein manuelles Lifecycle-Management nötig.


// Auto-play with pause on hover and focus
startAutoPlay(interval = 5000) {
  this.autoPlayInterval = setInterval(() => {
    if (!this.paused) this.next();
  }, interval);
},

stopAutoPlay() {
  clearInterval(this.autoPlayInterval);
  this.autoPlayInterval = null;
},

// Called from @mouseenter and @focusin on container
pauseAutoPlay() { this.paused = true; },

// Called from @mouseleave and @focusout on container
resumeAutoPlay(e) {
  // For focusout: only resume if focus left the component entirely
  if (e?.type === 'focusout' && this.$el.contains(e.relatedTarget)) return;
  this.paused = false;
},

init() {
  this.startAutoPlay(5000);
  this.initSwipe();
},

destroy() {
  this.stopAutoPlay();
}

7. ARIA-Rollen und Live-Region für Screenreader

Ein Carousel ohne ARIA-Anpassungen ist für Screenreader-Nutzer eine Blackbox. Der Container bekommt role="region" und aria-label="Bild-Galerie", damit Screenreader das Widget identifizieren können. Jeder Slide bekommt role="group" und aria-label="Slide 1 von 5" – das gibt Nutzern Orientierung über ihre Position in der Slideshow. Inactive Slides bekommen aria-hidden="true", damit ihr Inhalt nicht im virtuellen Cursor des Screenreaders landet.

Für automatische Slide-Wechsel ist eine aria-live-Region unverzichtbar. Ein verstecktes <div aria-live="polite" aria-atomic="true"> mit Klasse sr-only erhält bei jedem Slide-Wechsel einen neuen Text wie "Slide 2 von 5" – der Screenreader liest diesen Text vor, sobald der Nutzer keine andere Aktion ausführt. aria-atomic="true" stellt sicher, dass der gesamte Text als eine Einheit vorgelesen wird, nicht Wort für Wort. Das ist der sauberste Weg, Slide-Wechsel zu kommunizieren, ohne den DOM-Fokus zu bewegen.

8. Paginierungspunkte und Thumbnail-Navigation

Paginierungspunkte sind Buttons, keine Links oder Divs. Jeder Punkt bekommt aria-label="Slide 3 anzeigen" und aria-pressed="true/false". Alternativ kann die gesamte Dot-Navigation als role="tablist" mit role="tab"-Buttons implementiert werden, wenn die Slides konzeptuell wie Tabs funktionieren. In den meisten Fällen ist die einfachere Button-Variante vorzuziehen, weil das Tab-Pattern eine spezifischere Tastaturnavigation erfordert (nur Arrow-Tasten innerhalb der Tablist).

Thumbnail-Navigation ist eine Erweiterung, bei der kleine Vorschaubilder anstelle von Punkten die Paginierung steuern. Die Implementierung unterscheidet sich nicht wesentlich: Die Thumbnails sind Buttons, @click="goTo(index)" navigiert zum entsprechenden Slide, :aria-current="current === index ? 'true' : 'false'" zeigt den aktiven Zustand an. Das aktive Thumbnail bekommt einen visuellen Rahmen über eine Tailwind-Klasse, die via :class an den State gebunden ist. Für Lazy Loading der Thumbnail-Bilder bietet sich das loading="lazy"-Attribut an – Browser-native Lösung ohne JavaScript.

9. Alpine Carousel vs. Swiper.js im Vergleich

Swiper.js hat ohne Frage mehr Features – Virtual Slides für sehr lange Listen, CSS Scroll Snap Integration, komplexe Effekte wie Cards und Cube. Für einen Standard-Produktbild-Slider oder Hero-Banner auf einer Hyvä-Seite werden diese Features aber nie benötigt. Der Vergleich zeigt, wo die eigene Alpine-Implementierung klar überlegen ist: Bundle-Größe, Hyvä-Integration und volle Kontrolle über ARIA.

Kriterium Swiper.js Alpine.js nativ Vorteil Alpine
Bundle-Größe ~140 KB minified 0 KB extra Alpine.js ohnehin im Hyvä-Bundle
Touch-Swipe Eingebaut, konfigurierbar Pointer Events, ~15 Zeilen Kein Event-System-Konflikt
ARIA-Compliance Begrenzt, Workarounds nötig Vollständig kontrollierbar Jedes Attribut direkt setzbar
Tailwind CSS Eigene CSS-Datei, Konflikte Natives Tailwind-Markup Kein CSS-Override nötig
Auto-Play-Pause Konfigurierbar per Option mouseenter + focusin nativ WCAG 2.2.2 konform

Der einzige Bereich, in dem Swiper.js klar gewinnt, ist Virtual Slides für sehr lange Listen (Tausende von Slides). Das ist aber ein Nischenfall – Produktgalerien haben typischerweise 3–10 Bilder, Hero-Slider 2–5 Slides. Für alles darunter ist die Alpine-Implementierung die bessere Wahl: schneller geladen, besser zugänglich und vollständig im Tailwind-CSS-System.

Mironsoft

Alpine.js Komponenten · Hyvä Theme Entwicklung · Performance

Performante Slider für euren Hyvä-Shop?

Wir ersetzen schwere Carousel-Bibliotheken durch Alpine.js-native Implementierungen – leichter, zugänglicher und vollständig in euer Tailwind-Design integriert.

Performance-Audit

Analyse und Entfernung unnötiger JavaScript-Bibliotheken im Frontend

Slider-Entwicklung

Alpine.js Carousel mit Touch, Keyboard, Auto-Play und ARIA für Hyvä

Barrierefreiheit

WCAG 2.1 AA Compliance für Carousel-Komponenten und interaktive Elemente

10. Zusammenfassung

Ein produktionsreifer Alpine.js Carousel-Slider erfordert mehr als nur Vorwärts- und Rückwärts-Buttons. Touch-Swipe über Pointer Events, Tastaturnavigation mit Roving Tabindex, Auto-Play mit WCAG-konformer Pause-Logik und ARIA-Live-Regionen für Screenreader sind keine Extras – sie sind Voraussetzung für einen Slider, der auf allen Geräten und für alle Nutzer funktioniert. All das ist mit Alpine.js ohne externe Abhängigkeiten realisierbar und fügt sich nahtlos in Hyvä-Themes und Tailwind CSS ein.

Der entscheidende Vorteil gegenüber Swiper.js oder Slick: Null zusätzliche JavaScript-Bytes, keine Konflikte mit dem Alpine-Lifecycle, volle Kontrolle über Markup und ARIA-Attribute. Wer die in diesem Tutorial beschriebenen Patterns umsetzt, hat einen Slider, der Core Web Vitals nicht belastet, WCAG 2.1 AA erfüllt und von Tailwind vollständig stylebar ist.

Alpine.js Carousel — Das Wichtigste auf einen Blick

Touch-Swipe

Pointer Events statt separater Touch/Mouse-Handler. setPointerCapture verhindert Event-Verlust bei schnellen Swipes. touch-action: pan-y erlaubt vertikales Scrollen.

ARIA

role="region" am Container, role="group" pro Slide, aria-hidden auf inaktiven Slides, aria-live="polite" für Slide-Wechsel-Ankündigungen.

Auto-Play

setInterval in init(), clearInterval in destroy(). Pause bei mouseenter und focusin. Resume bei mouseleave und focusout mit contains()-Prüfung.

CSS-Transition

Nur opacity und transform animieren – läuft auf dem Compositor-Thread. Kein JavaScript-Animation, kein requestAnimationFrame, kein GSAP nötig.

11. FAQ: Alpine.js Carousel Slider

1Alpine.js für Produktbild-Slider in Hyvä?
Ja. Alpine.js ist in Hyvä ohnehin vorhanden. Slider mit Touch, Keyboard und ARIA ohne externe Bibliothek – kein zusätzliches Bundle-Gewicht.
2Pointer Events vs. Touch Events?
Pointer Events: ein Handler für Maus, Touch und Stylus. Moderner Standard, in allen aktuellen Browsern unterstützt. Kein separates touchstart/touchend nötig.
3Was ist setPointerCapture?
Hält alle Pointer Events auf dem Element – auch wenn der Finger über die Grenzen bewegt wird. Verhindert Event-Verlust bei schnellen Swipes.
4Auto-Play WCAG-konform pausieren?
Pause-Button als Mindestanforderung. Empfohlen: automatische Pause bei mouseenter und focusin, Resume bei mouseleave und focusout mit contains()-Prüfung.
5Was macht das inert-Attribut?
Entfernt alle Elemente des Containers aus Fokus-Reihenfolge und Accessibility Tree. Verhindert Tab durch versteckte Links in inaktiven Slides.
6Loop-Navigation implementieren?
In next(): Wenn current === total - 1, dann current = 0. In prev(): Wenn current === 0, dann current = total - 1. Via options.loop-Flag steuerbar.
7CSS-Transition vs. JavaScript-Animation?
CSS auf opacity/transform läuft auf dem Compositor-Thread – kein Ruckeln bei JavaScript-Last. JavaScript-Animation läuft auf dem Hauptthread.
8Wie viele Slides ohne Performance-Probleme?
Bis zu 50 statische Slides kein Problem. Bei mehr: Lazy Loading – Slides erst beim Navigieren in den DOM einfügen.
9Thumbnail-Navigation bauen?
Thumbnails als Buttons mit @click="goTo(index)". Aktiver Rahmen via :class. loading="lazy" für Bilder. aria-current="true" am aktiven Thumbnail.
10aria-live polite vs. assertive?
polite wartet auf das Ende der aktuellen Ausgabe. assertive unterbricht sofort. Für Slide-Wechsel immer polite – assertive nur für kritische Fehler.