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.
Inhaltsverzeichnis
- 1. Warum kein Swiper.js auf Hyvä-Seiten
- 2. Carousel-State mit Alpine.js aufbauen
- 3. CSS-Transition statt JavaScript-Animation
- 4. Touch-Swipe mit Pointer Events erkennen
- 5. Tastaturnavigation nach ARIA-Roving-Tabindex
- 6. Auto-Play mit Pause-on-Hover und Pause-on-Focus
- 7. ARIA-Rollen und Live-Region für Screenreader
- 8. Paginierungspunkte und Thumbnail-Navigation
- 9. Alpine Carousel vs. Swiper.js im Vergleich
- 10. Zusammenfassung
- 11. FAQ
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.