Vorher-Nachher-Bildvergleich mit Alpine.js: Slider ohne Bibliothek
AI generated
x-data
Alpine
Alpine.js · Bildvergleich · Case Study
Vorher-Nachher-Bildvergleich mit Alpine.js
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.

20 Min. Lesezeit x-data · Pointer Events · clip-path Alpine.js 3.x

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.

11. FAQ: Bildvergleichsslider mit Alpine.js

1Warum clip-path statt width?
clip-path ändert nur den sichtbaren Bereich und wird über die GPU gerendert, ohne Layout Reflow.
2Wie funktioniert der Griff?
Seine left Position ist an dieselbe Variable gebunden wie der clip-path, dadurch nie auseinanderlaufend.
3Wie werden Maus und Touch vereint?
Über die Pointer Events API mit identischem Verhalten für alle Eingabegeräte.
4Wofür setPointerCapture?
Damit Move Events auch beim Verlassen des Containers am Ausgangselement gebunden bleiben.
5Tastatur Bedienung möglich?
Ja, über ARIA Rolle slider plus Pfeiltasten, Home und End Handler.
6Warum ruckelt der Slider manchmal?
Ohne Drosselung verarbeitet Alpine.js jedes pointermove Event, requestAnimationFrame löst das.
7Auch vertikal möglich?
Ja, mit inset(Y% 0 0 0) und clientY statt clientX.
8Responsive geeignet?
Ja, da die Position als Prozentwert unabhängig von der Containerbreite gespeichert wird.
9Wie wird 0 bis 100 begrenzt?
Mit Math.min(100, Math.max(0, raw)) bei jeder Positionsberechnung.
10Lohnt sich Eigenbau statt jQuery?
Für neue Projekte fast immer, wegen kleinerer Bundle Größe und GPU Rendering.