Skip to content
← 1001 Widgets
N° 017 · ●○○

Reading bar

A bar that moves with scrolling and a reading time that counts down. The right exercise to learn to measure scroll without slowing the page.

CSSJS
What you'll learn
  • Compute how much of an element has scrolled
  • Animate with transform rather than width
  • Throttle the work with requestAnimationFrame
Why this language

Pourquoi une barre de lecture ? Sur un long article, on ne sait jamais combien il reste. Une fine barre en haut de l'écran répond à la question sans un mot.

Le calcul tient en une ligne : la distance déjà défilée, divisée par la distance qu'on peut défiler au total. On obtient un nombre entre 0 et 1.

Pourquoi en JavaScript simple ? Un écouteur de défilement, une division, un style. Le vrai sujet est ailleurs : ne rien faire de lourd pendant qu'on défile.

How it works
  1. LE HTML

    <div class="barre" aria-hidden="true"><i id="progression"></i></div>
    <article id="article">...</article>
  2. LE CSS

    La barre est pleine par défaut, et réduite avec scaleX : pas de recalcul de mise en page.

    .barre { position: fixed; top: 0; left: 0; right: 0; height: 4px; }
    .barre i { display: block; height: 100%; background: currentColor; transform-origin: left; transform: scaleX(0); }
  3. LE CALCUL

    scrollY : ce qu'on a défilé. scrollHeight - innerHeight : ce qu'on peut défiler.

    function part() {
      const max = document.documentElement.scrollHeight - innerHeight;
      return max > 0 ? Math.min(1, scrollY / max) : 1;
    }
  4. METTRE À JOUR UNE FOIS PAR IMAGE

    Le défilement envoie des dizaines d'événements : on ne dessine qu'une fois par image.

    let enAttente = false;
    addEventListener('scroll', () => {
      if (enAttente) return;
      enAttente = true;
      requestAnimationFrame(() => {
        document.getElementById('progression').style.transform = `scaleX(${part()})`;
        enAttente = false;
      });
    }, { passive: true });
  5. LE TEMPS RESTANT

    Environ 230 mots par minute pour une lecture à l'écran.

    const mots = document.getElementById('article').textContent.split(/\s+/).length;
    const minutesRestantes = () => Math.ceil(mots * (1 - part()) / 230);
Pitfalls

PIÈGE 1 : Animer width

Pitfall

Mauvais : barre.style.width = pourcent + '%'. À chaque défilement, le navigateur recalcule la mise en page.

Fix

Correct : transform: scaleX(), qui ne touche qu'à l'affichage.

PIÈGE 2 : Tout faire dans l'écouteur

Pitfall

Mauvais : calculer et dessiner à chaque événement scroll. Sur mobile, le défilement saccade.

Fix

Correct : requestAnimationFrame pour dessiner au plus une fois par image, et { passive: true } sur l'écouteur.

PIÈGE 3 : Diviser par zéro

Pitfall

Mauvais : scrollY / max sur une page plus courte que l'écran. max vaut 0, le résultat vaut NaN.

Fix

Correct : tester max > 0, et considérer qu'une page courte est lue en entier.

PERSONNALISER
  • Mesurez l'article seulement, et non toute la page, avec sa position getBoundingClientRect().
  • Affichez « Lu » et un petit pictogramme quand la barre est pleine.
  • Une barre verticale sur le côté pour un format magazine.
OPTIMISATION
  • Les navigateurs récents savent le faire en CSS pur avec animation-timeline: scroll(), sans une ligne de JavaScript.
  • Gardez le JavaScript comme solution de repli, détectée avec CSS.supports.
VERS REACT

Un hook useScrollProgress renvoie la valeur entre 0 et 1.

L'écouteur s'ajoute dans un useEffect et se retire au démontage.

Pour éviter un rendu par image, écrivez directement dans un useRef sur l'élément au lieu d'un useState.

Comments
0/1000

Loading comments…

Audience measurement

With your consent, this site measures its traffic with Google Analytics and the Meta pixel, via Google Tag Manager. Nothing is loaded until you choose, and you can change your mind at any time with the "Cookies" link at the bottom of the page.