Scroll Progress Bar am Seitenrand mit Alpine.js
AI generated
x-data
Alpine
Alpine.js · Scroll · Performance · UX Pattern
Scroll Progress Bar am Seitenrand mit Alpine.js
Lesefortschritt performant sichtbar machen

Eine Scroll Progress Bar zeigt Lesern eines langen Artikels auf einen Blick, wie viel Inhalt noch folgt und wie weit sie bereits gekommen sind. Mit Alpine.js lässt sich diese Fortschrittsanzeige in wenigen Zeilen umsetzen, performant an das Scroll Event gebunden und wahlweise als horizontale Leiste oben oder vertikal am Seitenrand.

11 Min. Lesezeit @scroll.window · requestAnimationFrame · x-bind:style Alpine.js 3.x · Blog Artikel · Lange Seiten

1. Warum eine Scroll Progress Bar Leser orientiert

Eine Scroll Progress Bar beantwortet für Leser eines langen Artikels eine einfache, aber wichtige Frage: wie viel Text liegt noch vor mir. Ohne dieses visuelle Signal orientieren sich Nutzer nur an der Länge des Scrollbalkens des Browsers, der auf mobilen Geräten oft kaum sichtbar ist und keine Rückmeldung darüber gibt, wie lang der eigentliche Artikelinhalt im Verhältnis zu Kommentaren, Footer oder verwandten Artikeln ist.

Studien zu Leseverhalten auf langen Content Seiten zeigen wiederholt, dass eine sichtbare Fortschrittsanzeige die wahrgenommene Länge eines Artikels reduziert und die Abbruchrate senkt, weil Leser ein konkretes Ziel vor Augen haben, statt endlos zu scrollen. Eine Scroll Progress Bar ist damit kein rein dekoratives Element, sondern ein messbares UX Werkzeug gegen Absprünge auf Blogartikeln und Dokumentationsseiten.

Mit Alpine.js lässt sich eine solche Fortschrittsanzeige ohne zusätzliche Bibliothek umsetzen. Die folgenden Abschnitte bauen eine vollständige Scroll Progress Bar Komponente: von der Berechnung des Fortschritts über performante Event Bindung, die Wahl zwischen horizontaler und vertikaler Darstellung, bis zur Kombination mit einer aktiven Inhaltsverzeichnis Verlinkung.

2. Fortschritt berechnen: scrollTop, scrollHeight, clientHeight

Der mathematische Kern jeder Scroll Progress Bar ist eine einfache Formel: der aktuelle Scroll Fortschritt in Prozent ergibt sich aus scrollTop geteilt durch die Differenz aus scrollHeight und clientHeight, multipliziert mit hundert. scrollTop gibt an, wie viele Pixel bereits nach oben gescrollt wurden, scrollHeight ist die gesamte Höhe des scrollbaren Inhalts, und clientHeight ist die sichtbare Höhe des Viewports.

Ein häufiger Fehler in dieser Berechnung: Entwickler nutzen scrollHeight allein als Nenner, statt die sichtbare Viewport Höhe abzuziehen. Das Ergebnis ist eine Fortschrittsanzeige, die niemals hundert Prozent erreicht, weil der letzte Bildschirm Inhalt eben noch sichtbar bleibt, während der Nutzer bereits am Ende der Seite angekommen ist. Die korrekte Formel berücksichtigt diesen Unterschied explizit und erreicht exakt hundert Prozent, sobald der Nutzer das Seitenende erreicht hat.


// Correct scroll progress calculation
function calculateScrollProgress() {
  const scrollTop = window.scrollY || document.documentElement.scrollTop;
  const scrollHeight = document.documentElement.scrollHeight;
  const clientHeight = document.documentElement.clientHeight;

  const scrollableDistance = scrollHeight - clientHeight;
  if (scrollableDistance <= 0) return 100; // page shorter than viewport

  return Math.min(100, (scrollTop / scrollableDistance) * 100);
}

3. Alpine Komponente mit @scroll.window und x-bind:style

Alpine bindet Scroll Events über den .window Modifier direkt an window, ohne dass ein manueller addEventListener Aufruf nötig wäre. Der berechnete Fortschritt landet als reaktive Zahl im x-data Objekt und wird über x-bind:style als Breite oder Höhe der eigentlichen Progress Bar gebunden. Diese enge Kopplung zwischen Scroll Position und CSS Eigenschaft ist der Kern der gesamten Scroll Progress Bar Komponente.

Wichtig ist, die Berechnung nicht nur beim Scroll Event, sondern auch einmal beim Initialisieren auszuführen, damit eine Seite, die mit einem Anker Link oder Browser Verlauf bereits mitten im Inhalt geladen wird, sofort den korrekten Fortschritt anzeigt, statt bei null zu starten und erst beim ersten Scroll Event nachzuziehen.


<div
  x-data="scrollProgress"
  @scroll.window="progress = calculateProgress()"
  class="fixed top-0 left-0 right-0 h-1 z-50"
>
  <div
    class="h-full bg-teal-500 transition-none"
    :style="`width: ${progress}%`"
  ></div>
</div>

// Alpine.data component for a top-of-page scroll progress bar
document.addEventListener('alpine:init', () => {
  Alpine.data('scrollProgress', () => ({
    progress: 0,

    init() {
      // Calculate once on load, not just on the first scroll event
      this.progress = this.calculateProgress();
    },

    calculateProgress() {
      const scrollTop = window.scrollY;
      const scrollableDistance = document.documentElement.scrollHeight - document.documentElement.clientHeight;
      if (scrollableDistance <= 0) return 100;
      return Math.min(100, (scrollTop / scrollableDistance) * 100);
    }
  }));
});

4. Performance: requestAnimationFrame statt jedes Scroll Events

Das Scroll Event feuert im Browser extrem häufig, teils mehrere hundert Mal pro Sekunde bei schnellem Scrollen mit Trackpad oder Maus Rad. Wird bei jedem einzelnen Event eine Neuberechnung und ein DOM Update ausgelöst, kann das auf leistungsschwächeren Geräten zu spürbarem Ruckeln führen, selbst wenn die Berechnung selbst trivial ist, weil jedes Style Update potenziell ein Repaint erzwingt.

Die Standardlösung ist, die eigentliche Aktualisierung über requestAnimationFrame zu drosseln: das Scroll Event setzt lediglich ein Flag, dass ein Update ansteht, und ein einzelner requestAnimationFrame Callback führt die tatsächliche Berechnung und das Style Update synchron zum nächsten Browser Frame aus. Dadurch wird die Scroll Progress Bar nie öfter aktualisiert, als der Bildschirm tatsächlich neu zeichnen kann, was Ruckeln effektiv verhindert.


// Throttling scroll progress updates with requestAnimationFrame
Alpine.data('scrollProgress', () => ({
  progress: 0,
  ticking: false,

  onScroll() {
    if (!this.ticking) {
      requestAnimationFrame(() => {
        this.progress = this.calculateProgress();
        this.ticking = false;
      });
      this.ticking = true;
    }
  },

  calculateProgress() {
    const scrollTop = window.scrollY;
    const scrollableDistance = document.documentElement.scrollHeight - document.documentElement.clientHeight;
    if (scrollableDistance <= 0) return 100;
    return Math.min(100, (scrollTop / scrollableDistance) * 100);
  }
}));

5. Horizontale Leiste vs. vertikale Bar am Seitenrand

Die klassische Variante der Scroll Progress Bar ist eine dünne, horizontale Leiste am oberen Rand der Seite, die über die volle Breite wächst. Diese Variante ist einfach umzusetzen und stört das Layout kaum, hat aber den Nachteil, dass sie auf breiten Desktop Bildschirmen visuell wenig auffällt und leicht mit einem Ladebalken verwechselt werden kann.

Die vertikale Variante am Seitenrand, meist rechts als schmaler Streifen, nutzt stattdessen die Höhe des Viewports als Skala und wächst von oben nach unten, während der Nutzer scrollt. Diese Darstellung fällt weniger mit Ladeindikatoren zusammen und lässt sich gut mit weiteren Elementen kombinieren, etwa kleinen Markierungen für einzelne Abschnitte entlang derselben vertikalen Achse. Technisch ändert sich an der Berechnung nichts, lediglich width wird durch height ersetzt und die Positionierung von top auf right umgestellt.


<!-- Vertical scroll progress bar on the page edge -->
<div
  x-data="scrollProgress"
  @scroll.window="onScroll()"
  class="fixed top-0 right-0 bottom-0 w-1 z-50 bg-slate-100/40"
>
  <div
    class="w-full bg-teal-500"
    :style="`height: ${progress}%`"
  ></div>
</div>

6. Kombination mit Abschnitts-Markern und aktiver TOC Verlinkung

Eine Scroll Progress Bar entfaltet ihren vollen Nutzen, wenn sie nicht nur den globalen Fortschritt zeigt, sondern zusätzlich anzeigt, in welchem Abschnitt des Artikels sich der Nutzer gerade befindet. Dafür lässt sich die Fortschrittslogik mit der Intersection Observer API kombinieren, die erkennt, welche Überschrift gerade im sichtbaren Bereich steht, und diese Information nutzt, um den entsprechenden Eintrag im Inhaltsverzeichnis optisch hervorzuheben.

Diese Kombination aus globaler Scroll Progress Bar und aktiver Abschnitts Hervorhebung im Inhaltsverzeichnis ist besonders bei langen technischen Artikeln wertvoll, weil Leser so nicht nur sehen, wie viel Text insgesamt noch folgt, sondern auch, in welchem thematischen Abschnitt sie sich gerade befinden, ohne zurückscrollen zu müssen, um die aktuelle Überschrift erneut zu lesen.

7. Styling, aria-hidden und prefers-reduced-motion

Eine Scroll Progress Bar ist ein rein visuelles, dekoratives Element ohne eigene interaktive Funktion, weshalb sie für Screenreader mit aria-hidden="true" ausgezeichnet werden sollte. Ein Screenreader Nutzer profitiert nicht von einer laufend aktualisierten Prozentzahl, die bei jedem Scroll Event vorgelesen würde, das wäre eher störend als hilfreich.

Zusätzlich sollte die Breiten oder Höhenänderung der Bar keine CSS Transition erhalten, die über wenige Millisekunden hinausgeht, da eine zu träge Animation bei schnellem Scrollen dem tatsächlichen Fortschritt sichtbar hinterherhinkt. Für Nutzer mit prefers-reduced-motion: reduce sollte jede Transition Dauer ohnehin auf null reduziert werden, da die Bar sich sonst als kontinuierliche Bewegung im Augenwinkel bemerkbar macht, was bei vestibulären Störungen zu Unwohlsein führen kann.


/* Respect reduced motion preference for the scroll progress bar */
.scroll-progress-fill {
  transition: width 100ms linear;
}

@media (prefers-reduced-motion: reduce) {
  .scroll-progress-fill {
    transition: none;
  }
}

8. Wiederverwendbare Alpine.data Komponente für die ganze Seite

Damit die Scroll Progress Bar nicht auf jeder Seite neu implementiert werden muss, lohnt sich eine globale Registrierung über Alpine.data, die einmal zentral definiert und per x-data="scrollProgress" auf jeder Seite eingebunden wird. In einem Hyvä Theme lässt sich diese Definition in einem zentralen JavaScript Modul ablegen, das über default.xml auf allen Seiten geladen wird, statt die Logik in jedem einzelnen Template zu wiederholen.

Eine Konfigurationsoption für den Zielcontainer ist sinnvoll, falls die Fortschrittsberechnung nicht die gesamte Seite, sondern nur einen bestimmten Artikel Container betreffen soll, etwa wenn Header und Footer nicht in die Berechnung einfließen sollen. Dafür wird statt document.documentElement ein per Parameter übergebenes Element referenziert, dessen eigene Scroll Position und Höhe die Grundlage der Berechnung bilden.

9. Implementierungsansätze im Vergleich

Es gibt mehrere technische Wege, eine Scroll Progress Bar umzusetzen, mit unterschiedlichen Kompromissen bei Performance und Browser Unterstützung.

Ansatz Trigger Vorteil Nachteil
Direktes Scroll Event @scroll.window ohne Drosselung Einfachste Implementierung Kann bei schwachen Geräten ruckeln
requestAnimationFrame Drosselung Scroll Event setzt Flag, rAF aktualisiert Sanft, an Frame Rate gekoppelt Etwas mehr Code notwendig
CSS Scroll-Driven Animation animation-timeline: scroll() Kein JavaScript, Compositor Thread Noch nicht in allen Browsern verfügbar
Intersection Observer für Abschnitte Beobachtung von Überschriften Elementen Ergänzt Bar um Abschnitts Kontext Löst allein nicht die globale Prozentanzeige

Für die meisten Blog und Dokumentationsseiten ist die Kombination aus @scroll.window mit requestAnimationFrame Drosselung der robusteste Ansatz, weil sie in allen aktuellen Browsern zuverlässig funktioniert und die Performance ohne zusätzliche Bibliothek unter Kontrolle hält. Native CSS Scroll-Driven Animations sind vielversprechend, aber aktuell noch nicht in allen relevanten Browsern verfügbar, weshalb sie sich derzeit nur als progressive Verbesserung eignen.

Mironsoft

Alpine.js UX Komponenten für Blogs und lange Content Seiten

Scroll Progress Bar für eure langen Artikel?

Wir bauen performante Scroll Progress Bars mit Alpine.js, inklusive Abschnitts-Markern, aktiver TOC Verlinkung und vollständiger Berücksichtigung von prefers-reduced-motion.

Performance Check

Bestehende Scroll Handler auf Ruckeln und Frame Drops prüfen

UX Komponenten

Scroll Progress Bar, Abschnitts-Marker und aktive Navigation aus einer Hand

Barrierefreiheit

aria-hidden und reduzierte Bewegung von Anfang an eingeplant

10. Zusammenfassung

Eine Scroll Progress Bar mit Alpine.js braucht im Kern nur drei Werte: scrollTop, scrollHeight und clientHeight, aus denen sich der Fortschritt in Prozent berechnen lässt. Die Bindung an @scroll.window in Kombination mit einer requestAnimationFrame Drosselung hält die Aktualisierung performant, selbst bei sehr schnellem Scrollen auf leistungsschwächeren Geräten.

Ob horizontale Leiste oben oder vertikale Bar am Seitenrand ist eine gestalterische Entscheidung, die technische Grundlage bleibt identisch. Kombiniert mit Abschnitts-Markern und aktiver Inhaltsverzeichnis Verlinkung wird aus der reinen Fortschrittsanzeige ein vollwertiges Orientierungswerkzeug für lange Artikel, das mit korrektem aria-hidden und Rücksicht auf prefers-reduced-motion auch barrierefrei bleibt.

Scroll Progress Bar mit Alpine.js — Das Wichtigste auf einen Blick

Formel

scrollTop geteilt durch (scrollHeight minus clientHeight), mal hundert, gedeckelt auf hundert Prozent.

Performance

requestAnimationFrame drosselt Updates auf die tatsächliche Bildwiederholrate.

Varianten

Horizontal oben oder vertikal am Seitenrand, dieselbe Berechnung, andere CSS Eigenschaft.

Barrierefreiheit

aria-hidden="true" und keine Transition bei prefers-reduced-motion: reduce.

11. FAQ: Scroll Progress Bar mit Alpine.js

1Wie berechne ich den Scroll Fortschritt?
scrollTop geteilt durch (scrollHeight minus clientHeight), mal hundert, gedeckelt auf hundert Prozent.
2Wie bindet man das Scroll Event?
Mit .window Modifier, @scroll.window ohne manuellen addEventListener Aufruf.
3Warum ruckelt sie manchmal?
Zu häufige Style Updates. requestAnimationFrame drosselt auf die Bildwiederholrate.
4Horizontal oder vertikal?
Gleiche Berechnung, horizontal ist der Klassiker, vertikal lässt sich besser mit Markern kombinieren.
5Kombination mit aktiver TOC Verlinkung?
Intersection Observer erkennt sichtbare Überschriften und hebt den passenden TOC Eintrag hervor.
6Braucht sie ARIA Attribute?
aria-hidden="true", da rein dekorativ, kein Vorlesen laufender Prozentwerte.
7Was hat prefers-reduced-motion damit zu tun?
Transition sollte bei aktivierter Einstellung entfernt werden wegen vestibulärer Störungen.
8Warum erreicht sie nie hundert Prozent?
clientHeight fehlt im Nenner, dadurch bleibt am Ende immer ein Rest übrig.
9Auf einen Artikel Container beschränken?
Ja, ein spezifisches Element referenzieren statt document.documentElement.
10Braucht es eine externe Bibliothek?
Nein, Alpine.js reicht vollständig aus, ohne zusätzliche Scroll Bibliothek.