mit vollständigem Keyboard Support
Eine Lightbox-Galerie ohne jQuery, ohne externe Libraries: Alpine.js verwaltet den Zustand, Pfeiltasten navigieren, Escape schließt, Touch-Swipe funktioniert auf Mobile und Lazy Loading hält den Initial-Load schlank.
Inhaltsverzeichnis
- 1. Warum Alpine.js für eine Galerie statt einer Bibliothek?
- 2. Datenstruktur: Bilder, Zustand und Navigation
- 3. Thumbnail-Grid mit Tailwind CSS und x-for
- 4. Lightbox-Overlay: Anzeige, Transition und Schließen
- 5. Keyboard-Navigation: Pfeiltasten, Escape und Home/End
- 6. Touch-Swipe für Mobile ohne externe Bibliothek
- 7. Lazy Loading mit IntersectionObserver
- 8. Bild-Preloading für flüssige Navigation
- 9. Galerie-Ansätze im direkten Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum Alpine.js für eine Galerie statt einer Bibliothek?
Galerie- und Lightbox-Bibliotheken wie Fancybox, GLightbox oder Swiper.js sind mächtig – und deshalb oft überdimensioniert. Sie bringen typischerweise mehrere Kilobyte JavaScript und CSS mit, die für einfache Produktgalerien oder Portfolios weit über das Notwendige hinausgehen. Schlimmer noch: Viele dieser Bibliotheken setzen jQuery voraus oder bringen eigene komplexe Event-Systeme mit, die mit Alpine.js und Tailwind CSS schlecht harmonieren. Das Ergebnis sind Konflikte, doppeltes JavaScript und aufgeblähte Bundles.
Alpine.js kann eine vollwertige Lightbox-Galerie mit allen wesentlichen Features – Keyboard-Navigation, Touch-Support, Lazy Loading, ARIA-Accessibility – in deutlich weniger Code realisieren, als eine externe Bibliothek allein wiegt. Der entscheidende Vorteil: Der Code ist dein eigener. Er passt zum bestehenden Styling-System, kann einfach erweitert werden und erzeugt keine Versionskonflikte. Im Hyvä-Magento-Kontext ist das besonders relevant, weil Hyvä grundsätzlich auf jQuery-freies Frontend setzt.
Die Implementierung hat vier Schichten: Die Datenstruktur verwaltet den Zustand (welches Bild ist offen, welcher Index ist aktiv). Das Thumbnail-Grid rendert die Vorschaubilder. Das Lightbox-Overlay zeigt das Vollbild mit Navigation. Der Keyboard-Handler und der Touch-Handler kümmern sich um die verschiedenen Eingabemethoden. Alpine.js verbindet alle diese Schichten mit wenigen Direktiven – kein separater Framework-Code, keine Build-Konfiguration, kein Event-Bus.
2. Datenstruktur: Bilder, Zustand und Navigation
Die Alpine-Komponente verwaltet ein Array von Bildobjekten, einen aktiven Index und einen Lightbox-Öffnungszustand. Jedes Bildobjekt enthält mindestens src (Vollbild-URL), thumb (Thumbnail-URL), alt (zugänglicher Alternativtext) und optional caption für eine Bildunterschrift. Die Navigation wird über zwei Methoden implementiert: prev() und next(), die den Index zyklisch ändern. Zyklische Navigation bedeutet: Nach dem letzten Bild kommt das erste, vor dem ersten das letzte – das verhindert Sackgassen in der Navigation.
Ein wichtiges Design-Entscheidung ist, ob die Galerie den Zustand für Lazy Loading separat verwaltet. Ein praktisches Muster: Ein loaded-Set speichert die Indizes der bereits geladenen Bilder. Wenn die Lightbox geöffnet wird, werden das aktuelle Bild sowie das nächste und das vorherige Bild vorgeladen – das macht das Weiternavigieren gefühlt sofort, ohne Ladezeit. Beim Weiternavigieren werden jeweils die nächsten Bilder im Hintergrund vorgeladen.
3. Thumbnail-Grid mit Tailwind CSS und x-for
Das Thumbnail-Grid wird mit x-for aus dem Bilder-Array gerendert. Jedes Thumbnail ist ein Button mit @click="open(index)" und trägt die korrekte ARIA-Auszeichnung: aria-label mit dem Alt-Text des Bildes, aria-haspopup="dialog" da es einen Dialog öffnet. Das Bild selbst erhält loading="lazy" für natives Browser-Lazy-Loading der Thumbnails, was den Initial-Load beschleunigt. Die Hover-Effekte werden mit Tailwind-Klassen umgesetzt – kein zusätzliches CSS nötig.
Ein visuelles Detail mit großer UX-Wirkung: Das aktuell in der Lightbox angezeigte Bild erhält im Grid einen Ring-Effekt. :class="{ 'ring-2 ring-teal-500': lightboxOpen && currentIndex === index }" markiert das aktive Thumbnail, auch wenn die Lightbox offen ist. Das gibt Nutzern, die per Keyboard navigieren, eine visuelle Referenz im Grid. Gleichzeitig verhindert alt="" auf dekorativen Thumbnails, dass Screen Reader redundante Beschreibungen vorlesen – das alt-Attribut gehört auf das Vollbild im Lightbox-Overlay.
// Alpine.js Gallery + Lightbox component
document.addEventListener('alpine:init', () => {
Alpine.data('imageGallery', (images = []) => ({
images,
currentIndex: 0,
lightboxOpen: false,
touchStartX: 0,
touchEndX: 0,
preloadedIndices: new Set(),
open(index) {
this.currentIndex = index;
this.lightboxOpen = true;
this.preloadAdjacent(index);
// Prevent background scroll
document.body.style.overflow = 'hidden';
},
close() {
this.lightboxOpen = false;
document.body.style.overflow = '';
},
prev() {
this.currentIndex = (this.currentIndex - 1 + this.images.length) % this.images.length;
this.preloadAdjacent(this.currentIndex);
},
next() {
this.currentIndex = (this.currentIndex + 1) % this.images.length;
this.preloadAdjacent(this.currentIndex);
},
preloadAdjacent(index) {
const toPreload = [
index,
(index + 1) % this.images.length,
(index - 1 + this.images.length) % this.images.length
];
toPreload.forEach(i => {
if (!this.preloadedIndices.has(i)) {
const img = new Image();
img.src = this.images[i].src;
this.preloadedIndices.add(i);
}
});
},
get currentImage() {
return this.images[this.currentIndex] || null;
}
}));
});
4. Lightbox-Overlay: Anzeige, Transition und Schließen
Das Lightbox-Overlay wird mit x-show="lightboxOpen" ein- und ausgeblendet und mit x-transition animiert. Das Overlay selbst trägt role="dialog", aria-modal="true" und aria-label mit dem aktuellen Bildtitel – alles reaktiv mit :aria-label. @click.self="close()" auf dem Overlay schließt die Lightbox, wenn außerhalb des Bildes geklickt wird. Das verhindert versehentliches Schließen beim Klick auf das Bild selbst.
Die Navigation-Buttons erhalten explizite aria-label-Attribute (aria-label="Vorheriges Bild", aria-label="Nächstes Bild"), da sie nur Icons enthalten. Ein Bildcounter (x-text="`${currentIndex + 1} von ${images.length}`") gibt sowohl sehenden als auch Screen-Reader-Nutzern Kontext über die Position in der Galerie. Wenn die Lightbox schließt, muss der Fokus auf das Thumbnail-Element zurückkehren, das die Lightbox ausgelöst hat – das wird mit einer gespeicherten Referenz auf document.activeElement vor dem Öffnen erreicht.
5. Keyboard-Navigation: Pfeiltasten, Escape und Home/End
Alpine.js macht Keyboard-Navigation deklarativ. Im Lightbox-Container wird @keydown.arrow-right="next()", @keydown.arrow-left="prev()" und @keydown.escape="close()" gesetzt. Damit diese Handler funktionieren, muss der Lightbox-Container fokussierbar sein (tabindex="-1") und beim Öffnen den Fokus erhalten. Ohne Fokus auf dem Container gehen die Keydown-Events ins Leere, weil kein fokussiertes Element in der Lightbox ist, das die Events empfängt.
Zusätzliche Keyboard-Shortcuts verbessern die UX: @keydown.home="currentIndex = 0" springt zum ersten Bild, @keydown.end="currentIndex = images.length - 1" zum letzten. Diese Shortcuts entsprechen den Konventionen für Listen-Navigation in ARIA. Wichtig: Alle Keyboard-Handler sollten .prevent für Pfeiltasten verwenden, um zu verhindern, dass die Seite beim Scrollen im Lightbox-Kontext scrollt. @keydown.arrow-right.prevent="next()" verhindert das horizontale Scrollen der Seite.
<!-- Lightbox Overlay with full keyboard support -->
<div
x-show="lightboxOpen"
x-transition:enter="transition ease-out duration-200"
x-transition:enter-start="opacity-0 scale-95"
x-transition:enter-end="opacity-100 scale-100"
x-transition:leave="transition ease-in duration-150"
x-transition:leave-start="opacity-100 scale-100"
x-transition:leave-end="opacity-0 scale-95"
class="fixed inset-0 z-50 flex items-center justify-center"
style="background: rgba(0,0,0,0.90);"
role="dialog"
aria-modal="true"
:aria-label="currentImage ? `Bild ${currentIndex+1} von ${images.length}: ${currentImage.alt}` : 'Galerie'"
tabindex="-1"
x-ref="lightbox"
@click.self="close()"
@keydown.arrow-right.prevent="next()"
@keydown.arrow-left.prevent="prev()"
@keydown.escape="close()"
@keydown.home.prevent="currentIndex = 0"
@keydown.end.prevent="currentIndex = images.length - 1"
@touchstart="touchStartX = $event.changedTouches[0].screenX"
@touchend="touchEndX = $event.changedTouches[0].screenX; touchEndX - touchStartX > 50 ? prev() : touchStartX - touchEndX > 50 ? next() : null">
<!-- Navigation: Previous -->
<button @click="prev()" aria-label="Vorheriges Bild"
class="absolute left-4 top-1/2 -translate-y-1/2 w-12 h-12 rounded-full bg-white/10 hover:bg-white/25 flex items-center justify-center text-white">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 19l-7-7 7-7"/></svg>
</button>
<!-- Full-size image -->
<template x-if="currentImage">
<img :src="currentImage.src" :alt="currentImage.alt"
class="max-w-full max-h-full object-contain rounded-lg shadow-2xl"
style="max-height: 85vh; max-width: 90vw;">
</template>
<!-- Caption + Counter -->
<div class="absolute bottom-4 left-0 right-0 text-center text-white">
<p x-text="currentImage?.caption" class="text-sm mb-1 opacity-80"></p>
<p x-text="`${currentIndex + 1} / ${images.length}`" class="text-xs opacity-50"></p>
</div>
<!-- Navigation: Next -->
<button @click="next()" aria-label="Nächstes Bild"
class="absolute right-4 top-1/2 -translate-y-1/2 w-12 h-12 rounded-full bg-white/10 hover:bg-white/25 flex items-center justify-center text-white">
<svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/></svg>
</button>
<!-- Close button -->
<button @click="close()" aria-label="Galerie schließen"
class="absolute top-4 right-4 w-10 h-10 rounded-full bg-white/10 hover:bg-white/25 flex items-center justify-center text-white">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/></svg>
</button>
</div>
6. Touch-Swipe für Mobile ohne externe Bibliothek
Touch-Swipe-Gesten sind für mobile Galerien unverzichtbar. Die Implementierung ohne externe Bibliothek ist überraschend einfach: touchstart speichert die X-Position des ersten Berührungspunkts, touchend vergleicht sie mit der Endposition. Ist die Differenz größer als ein Schwellwert (typischerweise 50px), wird navigiert. Positive Differenz (Finger nach rechts) bedeutet vorheriges Bild, negative Differenz (Finger nach links) bedeutet nächstes Bild – das entspricht der natürlichen Swipe-Richtung.
Ein Detail, das den Unterschied macht: changedTouches[0].screenX statt touches[0].screenX im touchend-Event – touches ist beim touchend-Event leer, weil kein Finger mehr auf dem Screen liegt. changedTouches enthält die Berührungspunkte, die sich seit dem letzten Event geändert haben. Das ist einer der häufigsten Bugs in eigenen Touch-Implementierungen. Für Pinch-to-Zoom kann die vertikale Distanz beider Fingers über touches[0] und touches[1] ausgewertet werden – das geht aber über eine Lightbox hinaus und ist für einfache Galerien nicht notwendig.
7. Lazy Loading mit IntersectionObserver
Natives Browser-Lazy-Loading über loading="lazy" funktioniert für Thumbnails ausgezeichnet und benötigt kein JavaScript. Für Vollbilder in der Lightbox ist das Muster anders: Bilder werden erst geladen, wenn sie tatsächlich angezeigt werden, nicht beim Seitenaufruf. Das spart erheblich Bandbreite bei großen Galerien. Alpine.js implementiert das über eine Custom Direktive oder direkt in der Galerie-Komponente: Das src-Attribut des Lightbox-Bildes wird erst beim Öffnen der Lightbox auf die Vollbild-URL gesetzt.
Für die Thumbnail-Galerie selbst kann ein IntersectionObserver in einem Alpine.js Plugin verwendet werden, um native Lazy-Loading-Unterstützung in Browsern zu ergänzen, die loading="lazy" noch nicht vollständig unterstützen – heute praktisch kein relevantes Problem mehr, aber als Muster lehrreich. Wichtiger ist das Preloading der Nachbarbilder beim Öffnen der Lightbox: Das preloadAdjacent()-Pattern aus Abschnitt 2 lädt die nächsten und vorherigen Bilder im Hintergrund, ohne die aktuelle Anzeige zu blockieren.
8. Bild-Preloading für flüssige Navigation
Ohne Preloading sieht der Nutzer beim Weiternavigieren in der Lightbox jedes Mal kurz einen leeren Bildbereich, während das nächste Bild lädt. Das zerstört das Gefühl einer flüssigen Galerie. Das Preloading-Pattern löst das: Beim Öffnen eines Bildes werden sofort das nächste und das vorherige Bild unsichtbar vorab geladen. new Image(); img.src = url lädt das Bild in den Browser-Cache, ohne es im DOM anzuzeigen. Beim tatsächlichen Navigieren ist das Bild bereits gecacht und erscheint sofort.
Das preloadedIndices-Set verhindert, dass ein Bild mehrfach geladen wird. Nach dem ersten Laden ist es im Browser-Cache und muss nicht erneut vom Server angefordert werden. Für sehr große Galerien mit vielen hochauflösenden Bildern kann das Preloading auf die nächsten zwei Bilder vorwärts und eines rückwärts beschränkt werden, um Bandbreite zu sparen. Das Muster ist auch in Alpine.js Carousels und Slideshows direkt übertragbar.
9. Galerie-Ansätze im direkten Vergleich
Die Wahl des richtigen Ansatzes für eine Galerie hängt von Anforderungen, Bundle-Budget und Wartbarkeit ab.
| Ansatz | Bundle-Größe | Keyboard-Support | Anpassbarkeit |
|---|---|---|---|
| Alpine.js (eigene Impl.) | 0 kB (in Alpine inkl.) | Vollständig, WCAG | Vollständig |
| Fancybox 5 | ~50 kB (JS + CSS) | Gut | Begrenzt (API) |
| GLightbox | ~20 kB (JS + CSS) | Grundlegend | Mittel (CSS) |
| Swiper.js | ~40 kB (JS + CSS) | Gut | Mittel (API) |
| Vanilla JS (kein Framework) | ~5 kB (eigener Code) | Manuell | Vollständig |
Für Hyvä Themes und Magento 2 ist die Alpine.js-Eigenimplementierung fast immer die richtige Wahl: kein zusätzliches Bundle, vollständige Kontrolle über Styling mit Tailwind, kein Konflikt mit bestehenden Alpine-Stores und ein sauberer Keyboard-Support, der WCAG 2.1 erfüllt. Externe Bibliotheken sind sinnvoll, wenn sehr spezielle Features gebraucht werden, die eine Eigenimplementierung unverhältnismäßig aufwendig machen – etwa 3D-Übergänge, Video-Einbettung oder Zoom-Funktionen.
Mironsoft
Alpine.js Galerie-Entwicklung, Hyvä Themes und Magento 2 Produktgalerien
Produktgalerie mit Alpine.js für deinen Magento-Shop?
Wir entwickeln performante, barrierefreie Produktgalerien und Lightboxes für Hyvä-Themes – ohne externe Bibliotheken, mit vollständigem Keyboard-Support und Touch-Optimierung.
Galerie-Entwicklung
Lightbox, Carousel und Thumbnail-Grid maßgeschneidert für Hyvä und Magento 2
Performance-Optimierung
Lazy Loading, Preloading und WebP-Integration für optimale Core Web Vitals
Accessibility
WCAG-konforme Keyboard-Navigation und Screen-Reader-Support für Produktbilder
10. Zusammenfassung
Eine Alpine.js Image Gallery mit Lightbox und Keyboard Support ist vollständig ohne externe Bibliotheken realisierbar. Die Kernbausteine: Alpine.data() für die Zustandsverwaltung mit Index und Öffnungszustand, x-for für das Thumbnail-Grid, x-show mit x-transition für das Lightbox-Overlay, deklarative Keyboard-Handler mit @keydown-Direktiven, Touch-Swipe über touchstart und touchend-Events, und Preloading der Nachbarbilder über das Image()-API.
Das Ergebnis ist eine Galerie, die null Kilobyte zusätzliches JavaScript benötigt, vollständig mit Tailwind CSS gestaltet wird, WCAG-konforme Keyboard-Navigation bietet und auf Mobile-Geräten mit Touch-Swipe bedienbar ist. Für Hyvä-Themes und Magento 2 ist das die natürliche Wahl: kein jQuery, keine externen Abhängigkeiten, kein Konflikt mit dem CSP-System, vollständige Kontrolle über das Styling.
Alpine.js Galerie mit Lightbox — Das Wichtigste auf einen Blick
Zustandsverwaltung
images-Array, currentIndex und lightboxOpen in Alpine.data(). prev() und next() navigieren zyklisch. preloadAdjacent() lädt Nachbarbilder vor.
Keyboard-Navigation
@keydown.arrow-right, arrow-left, escape, home, end auf dem fokussierungsfähigen Lightbox-Container (tabindex="-1"). .prevent verhindert Seiten-Scrollen.
Touch-Swipe
touchstart speichert screenX. touchend vergleicht mit changedTouches[0].screenX. Schwellwert 50px für prev() oder next(). Keine externe Bibliothek.
Preloading
new Image(); img.src = url lädt Bilder in Browser-Cache ohne DOM-Anzeige. preloadedIndices Set verhindert doppeltes Laden. Aktuelles + 2 Nachbarn vorladen.