Ein Bildvergleichsslider ohne externe Bibliothek
Ein Bildvergleichsslider zeigt zwei überlagerte Bilder und lässt Nutzer per Ziehen die Grenze zwischen Vorher und Nachher verschieben. Mit CSS clip-path, einheitlichen Pointer Events und Alpine.js als reaktiver Zustandsschicht entsteht diese Interaktion ganz ohne jQuery Plugin und ohne zusätzliches Slider Framework.
Inhaltsverzeichnis
- 1. Warum ein Bildvergleichsslider ein gutes Alpine.js Beispiel ist
- 2. Grundstruktur: zwei überlagerte Bilder und clip-path
- 3. x-data State: Sliderposition als reaktive Variable
- 4. Maus und Touch vereinheitlichen mit Pointer Events
- 5. Clip-Path Berechnung: Prozentwert in CSS übersetzen
- 6. Der Griff: visuelles Ziehelement positionieren
- 7. Tastatur Steuerung für Barrierefreiheit
- 8. Performance: requestAnimationFrame für smooth Updates
- 9. Bildvergleichsslider Ansätze im Vergleich
- 10. Zusammenfassung
- 11. FAQ
1. Warum ein Bildvergleichsslider ein gutes Alpine.js Beispiel ist
Ein Bildvergleichsslider gehört zu den Komponenten, die auf Landingpages, in Portfolios und bei Vorher Nachher Präsentationen von Umbauten oder Bildbearbeitungen ständig gebraucht werden. Die typische Lösung ist ein fertiges jQuery Plugin oder eine ganze Slider Bibliothek, obwohl die eigentliche Technik hinter einem Bildvergleichsslider denkbar einfach ist: zwei übereinandergelegte Bilder, von denen eines über CSS clip-path nur teilweise sichtbar ist, plus ein Griff, der diesen Ausschnitt per Ziehen verschiebt.
Alpine.js eignet sich für einen Bildvergleichsslider besonders gut, weil die gesamte Logik in einer einzigen reaktiven Zahl steckt: der Position des Sliders in Prozent. Diese eine Zahl steuert gleichzeitig den clip-path des oberen Bildes und die horizontale Position des Griffs, ganz ohne manuelles DOM Update. Wo ein jQuery Plugin schnell zehn Kilobyte und eigene CSS Dateien mitbringt, kommt der Bildvergleichsslider mit Alpine.js mit wenigen Zeilen aus.
Dieser Artikel zeigt den kompletten Weg von der Grundstruktur über einheitliche Pointer Events bis zur Tastatursteuerung. Am Ende steht ein Bildvergleichsslider, der auf Desktop mit der Maus, auf Mobilgeräten per Touch und über die Tastatur bedienbar ist, und der auch bei schneller Bewegung ruckelfrei bleibt.
2. Grundstruktur: zwei überlagerte Bilder und clip-path
Die Grundstruktur eines Bildvergleichsslider besteht aus einem relativ positionierten Container, in dem zwei Bilder exakt übereinanderliegen. Das untere Bild, meist das Nachher Bild, füllt den Container vollständig aus. Das obere Bild, meist das Vorher Bild, liegt absolut positioniert direkt darüber und wird über clip-path: inset(0 X% 0 0) von rechts beschnitten, wobei X die aktuelle Sliderposition in Prozent ist.
Diese Technik hat einen entscheidenden Vorteil gegenüber älteren Ansätzen, die mit width und overflow: hidden arbeiten: clip-path verändert nicht das Box Modell des Elements, sondern nur dessen sichtbaren Bereich. Dadurch bleibt das Bild in voller Originalgröße im DOM, es wird lediglich unterschiedlich viel davon angezeigt. Das verhindert Verzerrungen und macht den Bildvergleichsslider unabhängig von der tatsächlichen Bildbreite im Container.
<div
x-data="beforeAfterSlider()"
class="relative w-full aspect-video overflow-hidden rounded-xl select-none"
>
<!-- After image: fills the container completely -->
<img src="/media/after.jpg" alt="Nachher" class="absolute inset-0 w-full h-full object-cover">
<!-- Before image: clipped from the right based on slider position -->
<img
src="/media/before.jpg"
alt="Vorher"
class="absolute inset-0 w-full h-full object-cover"
:style="`clip-path: inset(0 ${100 - position}% 0 0)`"
>
</div>
3. x-data State: Sliderposition als reaktive Variable
Der komplette Zustand eines Bildvergleichsslider reduziert sich auf eine einzige Zahl zwischen 0 und 100, die Position in Prozent von links. Alle abgeleiteten Werte, der clip-path des oberen Bildes und die left Position des Griffs, werden direkt aus dieser einen Variable berechnet, nie separat gespeichert. Das verhindert, dass Griff und Bildausschnitt auseinanderlaufen, ein häufiges Problem bei jQuery Implementierungen, die beide Werte getrennt aktualisieren.
Zusätzlich zur Position braucht der Bildvergleichsslider ein Flag, ob gerade gezogen wird, und eine Referenz auf den Container, um bei Mausbewegungen die relative Position innerhalb des Elements zu berechnen. Alpine.js stellt über $refs und x-ref genau diesen Zugriff auf das DOM Element bereit, ohne dass ein zusätzlicher document.querySelector Aufruf nötig wäre.
function beforeAfterSlider() {
return {
position: 50, // percentage from left, single source of truth
isDragging: false,
startDrag() {
this.isDragging = true;
},
stopDrag() {
this.isDragging = false;
},
// Convert a clientX coordinate into a 0-100 percentage relative to the container
updateFromClientX(clientX) {
const rect = this.$refs.container.getBoundingClientRect();
const raw = ((clientX - rect.left) / rect.width) * 100;
this.position = Math.min(100, Math.max(0, raw));
},
};
}
4. Maus und Touch vereinheitlichen mit Pointer Events
Ein häufiger Fehler bei einem selbst gebauten Bildvergleichsslider ist, separate Handler für mousedown/mousemove und touchstart/touchmove zu schreiben. Das verdoppelt den Code und führt regelmäßig zu Inkonsistenzen zwischen Desktop und Mobilgerät. Die Pointer Events API löst dieses Problem, indem sie Maus, Touch und Stift Eingaben unter einem einzigen Event Typ vereint: pointerdown, pointermove und pointerup funktionieren identisch für alle Eingabegeräte.
Entscheidend für einen ruckelfreien Bildvergleichsslider ist setPointerCapture(). Ohne diesen Aufruf verliert das Element den Bezug zum Pointer, sobald der Cursor während des Ziehens den Container verlässt, etwa bei schneller Mausbewegung. Mit event.target.setPointerCapture(event.pointerId) bleiben alle nachfolgenden pointermove Events an das ursprüngliche Element gebunden, unabhängig davon, wo sich der Cursor gerade befindet.
function beforeAfterSlider() {
return {
position: 50,
isDragging: false,
onPointerDown(event) {
this.isDragging = true;
// Keep receiving move events even if the cursor leaves the element
event.target.setPointerCapture(event.pointerId);
this.updateFromClientX(event.clientX);
},
onPointerMove(event) {
if (!this.isDragging) return;
this.updateFromClientX(event.clientX);
},
onPointerUp(event) {
this.isDragging = false;
event.target.releasePointerCapture(event.pointerId);
},
updateFromClientX(clientX) {
const rect = this.$refs.container.getBoundingClientRect();
const raw = ((clientX - rect.left) / rect.width) * 100;
this.position = Math.min(100, Math.max(0, raw));
},
};
}
5. Clip-Path Berechnung: Prozentwert in CSS übersetzen
Die clip-path Syntax für einen Bildvergleichsslider nutzt die inset() Funktion mit vier Werten in der Reihenfolge oben, rechts, unten, links. Da der obere Bildbereich von rechts beschnitten werden soll, bleibt nur der zweite Wert relevant: inset(0 X% 0 0), wobei X die Differenz zwischen 100 und der aktuellen Sliderposition ist. Bei einer Sliderposition von 30 Prozent wird das Bild also um 70 Prozent von rechts beschnitten, sodass die linken 30 Prozent des Vorher Bildes sichtbar bleiben.
Alternativ zu clip-path lässt sich derselbe Effekt mit clip: rect() erreichen, das aber als veraltet gilt und in modernen Browsern keine Vorteile bringt. Ein Bildvergleichsslider mit clip-path profitiert zusätzlich davon, dass moderne Browser diese Eigenschaft über die GPU rendern, was bei häufigen Updates während des Ziehens spürbar performanter ist als eine Breitenänderung über width, die einen kompletten Reflow des Layouts auslöst.
/* Static fallback styling — the dynamic clip-path value comes from Alpine :style */
.comparison-slider-before {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: cover;
will-change: clip-path; /* hint the browser this property animates */
}
/* Example computed value at position = 35 */
.comparison-slider-before-example {
clip-path: inset(0 65% 0 0);
}
6. Der Griff: visuelles Ziehelement positionieren
Der Griff eines Bildvergleichsslider ist ein rein visuelles Element, das die aktuelle Position sichtbar macht und dem Nutzer zeigt, wo gezogen werden kann. Technisch ist der Griff eine absolut positionierte Trennlinie mit einem runden Ziehknopf in der Mitte, deren left Eigenschaft direkt an dieselbe position Variable gebunden ist, die auch den clip-path steuert. Weil beide Werte aus derselben Quelle stammen, ist ein Auseinanderlaufen von Griff und Bildgrenze technisch ausgeschlossen.
Für eine gute Nutzererfahrung sollte der gesamte Container des Bildvergleichsslider als Ziehfläche fungieren, nicht nur der schmale Griff selbst. Ein Klick irgendwo im Bild sollte den Slider sofort an diese Position springen lassen, bevor das Ziehen fortgesetzt wird. Diese Erwartung erfüllt der onPointerDown Handler bereits, indem er updateFromClientX direkt beim ersten Klick aufruft, nicht erst bei der ersten Bewegung.
<div
x-data="beforeAfterSlider()"
x-ref="container"
@pointerdown="onPointerDown($event)"
@pointermove="onPointerMove($event)"
@pointerup="onPointerUp($event)"
class="relative w-full aspect-video overflow-hidden rounded-xl select-none cursor-ew-resize"
>
<img src="/media/after.jpg" alt="Nachher" class="absolute inset-0 w-full h-full object-cover">
<img src="/media/before.jpg" alt="Vorher" class="absolute inset-0 w-full h-full object-cover"
:style="`clip-path: inset(0 ${100 - position}% 0 0)`">
<!-- Divider line + drag handle, bound to the same position value -->
<div class="absolute inset-y-0 w-0.5 bg-white shadow-lg" :style="`left: ${position}%`">
<div class="absolute top-1/2 -translate-y-1/2 -translate-x-1/2 w-10 h-10 bg-white rounded-full shadow-xl flex items-center justify-center">
<span class="text-slate-700 text-xs font-bold">↔</span>
</div>
</div>
</div>
7. Tastatur Steuerung für Barrierefreiheit
Ein rein per Maus oder Touch bedienbarer Bildvergleichsslider schließt Tastaturnutzer aus, obwohl die Umsetzung einer Tastaturalternative nur wenige Zeilen zusätzlichen Code erfordert. Der Griff bekommt tabindex="0", die ARIA Rolle slider sowie aria-valuemin, aria-valuemax und aria-valuenow, damit Screenreader die aktuelle Position korrekt ansagen. Über @keydown.left und @keydown.right lässt sich die Position dann in kleinen Schritten verändern.
Für einen barrierefreien Bildvergleichsslider sollten zusätzlich @keydown.home und @keydown.end die Position auf 0 beziehungsweise 100 Prozent setzen, analog zum Verhalten nativer Range Inputs. Diese kleine Ergänzung macht den Unterschied zwischen einer rein dekorativen Spielerei und einer Komponente, die tatsächlich von allen Nutzergruppen bedient werden kann.
function beforeAfterSlider() {
return {
position: 50,
step: 2,
onKeydown(event) {
const actions = {
ArrowLeft: () => this.position = Math.max(0, this.position - this.step),
ArrowRight: () => this.position = Math.min(100, this.position + this.step),
Home: () => this.position = 0,
End: () => this.position = 100,
};
if (actions[event.key]) {
event.preventDefault();
actions[event.key]();
}
},
};
}
8. Performance: requestAnimationFrame für smooth Updates
Bei schnellen Mausbewegungen kann ein Bildvergleichsslider ins Stottern geraten, wenn jedes einzelne pointermove Event sofort eine Alpine.js Reaktivitäts Aktualisierung auslöst. Browser feuern pointermove deutlich häufiger, als das Layout tatsächlich neu gezeichnet werden kann, typischerweise mehrere hundert Mal pro Sekunde bei hochauflösenden Eingabegeräten. Ohne Drosselung verarbeitet Alpine.js jedes einzelne Event, was zu unnötiger Arbeit führt.
Die Lösung ist, die tatsächliche State Aktualisierung an requestAnimationFrame zu koppeln. Der letzte bekannte Mausposition Wert wird in einer einfachen Variable zwischengespeichert, und erst der nächste Animation Frame übernimmt diesen Wert in die reaktive position Eigenschaft. Dadurch aktualisiert sich der Bildvergleichsslider maximal so oft, wie der Bildschirm tatsächlich neu zeichnet, üblicherweise 60 Mal pro Sekunde, unabhängig davon, wie viele pointermove Events der Browser zwischendurch feuert.
function beforeAfterSlider() {
return {
position: 50,
isDragging: false,
pendingClientX: null,
rafScheduled: false,
onPointerMove(event) {
if (!this.isDragging) return;
this.pendingClientX = event.clientX;
this.scheduleUpdate();
},
// Coalesce many pointermove events into one update per animation frame
scheduleUpdate() {
if (this.rafScheduled) return;
this.rafScheduled = true;
requestAnimationFrame(() => {
const rect = this.$refs.container.getBoundingClientRect();
const raw = ((this.pendingClientX - rect.left) / rect.width) * 100;
this.position = Math.min(100, Math.max(0, raw));
this.rafScheduled = false;
});
},
};
}
9. Bildvergleichsslider Ansätze im Vergleich
Für einen Bildvergleichsslider existieren mehrere technische Umsetzungen mit unterschiedlichen Kompromissen bei Performance, Bundle Größe und Browser Unterstützung.
| Ansatz | Bundle Größe | Rendering | Eignung |
|---|---|---|---|
| clip-path plus Alpine.js | 0 KB zusätzlich | GPU beschleunigt | Standardfall, alle Projektgrößen |
| width plus overflow:hidden | 0 KB zusätzlich | Layout Reflow pro Update | Nur bei sehr einfachen Fällen |
| jQuery Vergleichs Plugin | circa 15 bis 25 KB gzip | Abhängig vom Plugin | Nur in bestehenden jQuery Projekten |
| SVG Clip Path Overlay | 0 KB zusätzlich | GPU beschleunigt | Komplexe Formen jenseits Rechtecke |
In der Praxis überzeugt clip-path in Kombination mit Alpine.js bei einem Bildvergleichsslider durch die Kombination aus minimaler Codemenge, keiner zusätzlichen Bundle Größe und GPU beschleunigtem Rendering. Nur bei sehr exotischen Beschneidungsformen jenseits einer einfachen vertikalen Trennlinie lohnt sich der zusätzliche Aufwand einer SVG basierten Lösung.
Mironsoft
Alpine.js Komponenten für Hyvä, Magento und individuelle Frontends
Ein eigener Bildvergleichsslider oder eine andere Alpine.js Komponente?
Wir entwickeln individuelle Alpine.js Komponenten, von interaktiven Bildvergleichen über Galerien bis zu komplexen Formularen, sauber integriert in euer bestehendes Hyvä oder Magento Frontend.
Konzeption
Interaktionsmuster und Performance Anforderungen klären
Umsetzung
Pointer Events, clip-path und Tastatur Bedienung aus einer Hand
Integration
Saubere Einbindung in bestehende Hyvä und Magento Frontends
10. Zusammenfassung
Ein performanter Bildvergleichsslider braucht keine externe Bibliothek, sondern nur drei Zutaten: CSS clip-path zum Beschneiden des oberen Bildes, die Pointer Events API zur einheitlichen Behandlung von Maus, Touch und Stift, und Alpine.js als schlanke reaktive Schicht, die eine einzige Positionsvariable in mehrere abgeleitete Darstellungen übersetzt. Diese Kombination spart gegenüber jQuery Plugins mehrere Kilobyte und liefert gleichzeitig GPU beschleunigtes Rendering.
Für den produktiven Einsatz kommen zwei weitere Bausteine dazu: eine Tastatursteuerung mit ARIA Slider Rolle für Barrierefreiheit, und eine Drosselung der State Updates über requestAnimationFrame, damit der Bildvergleichsslider auch bei schneller Mausbewegung ruckelfrei bleibt. Beide Ergänzungen zusammen benötigen weniger als zwanzig zusätzliche Zeilen Code.
Bildvergleichsslider mit Alpine.js — Das Wichtigste auf einen Blick
Technik
CSS clip-path: inset() beschneidet das obere Bild abhängig von einer einzigen Prozentvariable.
Eingabe
Pointer Events (pointerdown/pointermove/pointerup) vereinen Maus, Touch und Stift in einem Handler Satz.
Barrierefreiheit
ARIA Rolle slider, Tastatursteuerung mit Pfeiltasten, Home und End.
Performance
requestAnimationFrame koppelt State Updates an die tatsächliche Bildwiederholrate.