Progress Bar
Une piste de 4 px collée en tête d'un cadre de 280 px par position: sticky, dont le remplissage suit le défilement du cadre en scaleX(0 → 1) grâce à animation-timeline: scroll(nearest) — zéro JavaScript, zéro id, autant de copies que de cadres. Mesuré : scaleX = scrollTop ÷ 524 au millième, 0,5 à 262 px, 1 au dernier pixel ; la page qui défile autour ne bouge pas la barre.
Mis à jour le
Barre de progression
Scrollez pour voir la barre se remplir. Utilise animation-timeline: scroll() pour lier l'animation au défilement du conteneur.
Cet effet est gratuit — la catégorie Scroll en contient 70 au total, dont 12 gratuits. Effect.Labs regroupe 811 effets vanilla. Découvrir la catégorie →
Exemples d'usage
Notice de montage
9 étapes · 35 min2 montants, 5 tablettes, 1 fond, 24 vis, 8 chevilles, 1 clé.
Enfoncez les 8 chevilles dans les montants, côté intérieur.
Quatre vis de 40 mm dans les montants, sans forcer.
Même geste ; le cadre tient maintenant debout.
Trois tablettes posées sur les chevilles, à 40 cm d'écart.
Meuble à plat, panneau aligné, un clou tous les 15 cm.
Mesurez les deux diagonales : elles doivent être égales.
Quatre patins feutre sous les montants.
Équerre anti-basculement et cheville adaptée au mur.
Transcription
41 min · 6 200 motsLéa (hôte) — Bienvenue dans Atelier Sonore. Aujourd'hui, on parle de ce qui casse, et de ce qu'on en fait : un grille-pain, un téléphone, une machine à laver.
Marc — Il y a dix ans, j'ai ouvert un atelier de réparation dans une ancienne quincaillerie. On m'a dit que ça ne tiendrait pas six mois.
Léa — Qu'est-ce qui arrive le plus souvent sur ton établi ?
Marc — Des aspirateurs et des cafetières. Dans huit cas sur dix, c'est un fusible thermique à deux euros ou un joint. Le reste du temps, c'est la carte.
Léa — Et l'indice de réparabilité, ça change quelque chose ?
Marc — Pour les clients, oui : ils regardent la note avant d'acheter. Pour nous, ce qui compte, c'est la pièce détachée disponible cinq ans après, et le schéma.
Léa — Tu formes aussi des gens ?
Marc — Un samedi par mois. Les gens viennent avec leur objet, on l'ouvre ensemble. La plupart repartent avec l'objet réparé et l'envie de recommencer.
Léa — On fait une courte pause, et on revient avec les questions des auditeurs.
Léa — Première question, de Nadia à Lyon : est-ce que ça vaut le coup de réparer un téléphone dont la batterie est collée ?
Marc — Oui, avec une plaque chauffante et de la patience. La colle ramollit à soixante degrés ; la batterie coûte vingt euros. L'heure de main-d'œuvre, c'est le vrai prix.
Léa — Tom, à Nantes, demande quels outils acheter en premier.
Marc — Un multimètre, un jeu de tournevis de précision, et une lampe. Le reste vient avec les objets qu'on ouvre.
Léa — Et les objets que tu refuses de réparer ?
Marc — Tout ce qui a une cellule lithium fendue, et tout ce qui n'a ni schéma ni pièce. Je le dis dès le premier jour : je préfère perdre un client que vendre une réparation qui ne tiendra pas.
Comment ça marche
Le HTML vendu tient en trois niveaux : le cadre .demo-preview (flex centré, 100% × 280px, overflow: hidden, position: relative, fond #0a0a0f, texte blanc, sans-serif, et la règle box-sizing: border-box pour tout ce qu'il contient), le bloc défilant .sdc-scroll-container (100% × 280px, overflow-y: auto, scroll-behavior: smooth, barre de défilement de 4 px stylée par ::-webkit-scrollbar) et, dedans, dans l'ordre du flux, la piste .sdc-progress-bar (position: sticky; top: 0, 100% × 4px, fond blanc à 10 %, z-index: 10) qui contient le remplissage .sdc-progress-fill (hauteur 100 %, dégradé 90° #8b5cf6 → #ec4899, transform-origin: left, transform: scaleX(0)), puis le contenu .sdc-progress-content : padding: 20px, 800 px de haut, colonne flex à gap: 16px, un h3 de 1 rem graisse 700, un p de 0,78 rem en blanc à 50 % (interligne 1,6) et quatre .sdc-progress-block de 120 px (fond blanc à 4 %, bord blanc à 8 %, rayon 12 px). Sous le cadre, un span.sdc-scroll-hint « Scrollez ↓ » en absolu à 8 px du bas, 0,65 rem, blanc à 30 %, qui pulse jusqu'à 70 % toutes les 2 s. Le champ JavaScript est vide. Mesuré : scrollHeight 804 (800 px de contenu + 4 px de piste, qui est dans le flux), course 524 px.
Le moteur tient en deux déclarations sur .sdc-progress-fill : animation: sdcProgressGrow linear et animation-timeline: scroll(nearest). scroll() remplace l'horloge de l'animation par une timeline de progression de défilement anonyme : 0 % quand le défileur est en haut, 100 % à scrollHeight − clientHeight ; nearest (la valeur par défaut, écrite ici en toutes lettres) désigne le plus proche ancêtre qui défile — .sdc-scroll-container, pas la page — sur l'axe du bloc. Les keyframes vont de scaleX(0) à scaleX(1) ; linear n'est pas un détail : toute autre courbe déforme la proportion (mesuré avec ease-out : 0,68 à mi-course). Mesuré dans Chromium, page vierge, molette par crans de 130 px : 0,248 à 130 px, 0,496 à 260, 0,744 à 390, 0,992 à 520, 1 à 524 — scaleX = scrollTop ÷ 524 au millième ; un scrollTop = 262 posé par script donne 0,5 exactement. getAnimations() renvoie une animation sur une ScrollTimeline dont currentTime est un pourcentage (24,8 à 130 px), finished au bout, running à nouveau dès qu'on remonte. La piste collée ne bouge pas d'un pixel pendant que le titre passe dessous (mesuré : piste à y = 0 du cadre sur toute la course, h3 à −98 puis −492 px). Comme c'est un transform et non une largeur, le dégradé est peint sur toute la largeur puis comprimé : à 50 %, on voit tout le dégradé violet → rose sur la moitié gauche, et la pointe est toujours rose.
Le keyframe from { transform: scaleX(0) } est écrit explicitement, et ce n'est pas redondant. Sans lui, le point de départ est la valeur sous-jacente de l'élément, scaleX(0), une matrice singulière : Chromium interpole quand même les fonctions terme à terme (0,25 / 0,5 / 0,75 aux quarts), mais WebKit 26.5, mesuré, retourne 0,0625 / 0,25 / 0,5625 — le carré de la progression, une barre au quart quand la lecture est à moitié — alors que sa timeline est juste (currentTime 25, 50, 75 %). Avec le from, WebKit rend 0,25 / 0,5 / 0,75 / 1 comme Chromium. L'autre point à connaître est ce que nearest choisit vraiment. Mesuré, effet posé entre 2 000 px de contenu avant et après : la page défilée de 1 900 puis 2 500 px laisse la barre à 0 ; une molette de 262 px sur le cadre la met à 0,5, et la page ne bouge pas. Un second exemplaire dont le contenu ne fait que 200 px, sans débordement : la timeline lui est bien attachée mais reste inactive (currentTime nul), barre vide — elle ne remonte pas à la page. Deux copies sur la même page : aucun id, aucun script, chaque barre ne lit que son cadre. Le scroll-behavior: smooth n'agit pas sur la molette dans Chromium ; un scrollTop = 262 posé par script se relit 0 tout de suite et arrive en 249 ms (493 ms dans Firefox).
Accessibilité
- prefers-reduced-motion absent du code : mesuré sous émulation, rien ne change — la barre suit le défilement (0,248 à 130 px), l'indice pulse (
sdcHintPulse running) et lesmoothreste. Rien n'est caché : le remplissage ne bouge que du geste du lecteur, à la proportion exacte, ce qui en fait un indicateur et non une animation autonome (WCAG 2.3.3 vise le mouvement non essentiel). Ajoutez tout de même@media (prefers-reduced-motion: reduce) { .sdc-scroll-hint { animation: none } .sdc-scroll-container { scroll-behavior: auto } }pour la pulsation et les défilements programmés. - Clavier : mesuré, Tab donne le focus au cadre sans
tabindexdans Chromium et Firefox (activeElement=.sdc-scroll-container) ; flèche bas avance de 40 px (barre à 7,6 %), Page suivante de 285 px (54 %), Fin va au bout (100 %). WebKit ne focalise pas un bloc défilant par Tab (mesuré : focus resté surbody, flèche bas sans effet) : ajouteztabindex="0", unrole="region"avecaria-label, et un style:focus-visible— l'anneau par défaut entoure tout le cadre. - Contraste (sur
#0a0a0f) : titre blanc 19,8:1 ; paragraphe blanc à 50 % 5,4:1 à 12,5 px (seuil 4,5:1 tenu) ; remplissage 4,7:1 (violet) à 5,6:1 (rose) sur le fond et 3,7 / 4,4:1 sur la piste — au-dessus des 3:1 demandés à un composant graphique. La piste vide, blanc à 10 %, vaut 1,26:1 : à peine visible — c'est exactement ce que voit un visiteur Firefox. L'indice à 30 % vaut 2,6:1 au repos, de 1,2:1 à 1,8:1 sous la pulsation (la couleur à 30 % et l'opacité de 0,3 à 0,7 se multiplient) : posezaria-hidden="true"dessus ou supprimez lespan, il est décoratif et son texte est en dur. - Lecteurs d'écran : la piste et son remplissage sont deux
divvides, rien n'est annoncé, et la valeur n'existe que dans le compositeur — aucune règle CSS ne peut l'écrire dans un attribut. Si la progression a un sens (règlement à lire jusqu'au bout, attestation), il faut unrole="progressbar"dontaria-valuenowest mis à jour par un écouteurscrollde quatre lignes (c'est ce que fait Introduction, fx-0575) ; sinonaria-hidden="true"sur.sdc-progress-bar. Au doigt, le défilement est natif (aucun écouteur) ;overscroll-behavior: containévite d'entraîner la page en bout de cadre.
Compatibilité navigateur
CSS seul : animation-timeline: scroll() est la seule propriété qui fixe le plancher — Chrome et Edge 115, Safari 26 et iOS 26, Firefox aucune version (mesuré dans Firefox 153 : CSS.supports('animation-timeline: scroll()') faux, getAnimations() vide). Le reste est classique : position: sticky, transform, linear-gradient, @keyframes, overflow-y ; scroll-behavior et ::-webkit-scrollbar sont cosmétiques. Zéro dépendance, champ JavaScript vide. Le from explicite des keyframes est exigé par WebKit (voir la mécanique) : sans lui, Safari 26 rend le carré de la progression.
Mesuré dans Firefox, page vierge, molette par crans de 130 px puis scrollTop posé à 50 et 100 % : le remplissage reste à scaleX(0) du début à la fin — la piste de 4 px (blanc à 10 %, 1,26:1) est vide et presque invisible, tout le reste fonctionne. Rien n'est caché, rien ne se superpose : le visiteur Firefox n'a simplement pas de barre. Même chose avant Chrome 115 et Safari 26. Le code vendu ne contient pas de repli @supports ; deux choix : masquer la piste là où la timeline manque — @supports not (animation-timeline: scroll()) { .sdc-progress-bar { display: none } } — ou prendre le jumeau en JavaScript, Introduction (fx-0575), qui fait la même barre avec un écouteur scroll et tourne dans Firefox.
Le code
Copiez les trois blocs dans votre page. Aucune dépendance.
<div class="sdc-scroll-container">
<div class="sdc-progress-bar">
<div class="sdc-progress-fill"></div>
</div>
<div class="sdc-progress-content">
<h3>Barre de progression</h3>
<p>Scrollez pour voir la barre se remplir. Utilise animation-timeline: scroll() pour lier l'animation au défilement du conteneur.</p>
<div class="sdc-progress-block"></div>
<div class="sdc-progress-block"></div>
<div class="sdc-progress-block"></div>
<div class="sdc-progress-block"></div>
</div>
</div>
<span class="sdc-scroll-hint">Scrollez ↓</span>
.sdc-scroll-container {
width: 100%;
height: 280px;
overflow-y: auto;
position: relative;
scroll-behavior: smooth;
}
.sdc-scroll-container::-webkit-scrollbar {
width: 4px;
}
.sdc-scroll-container::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.05);
}
.sdc-scroll-container::-webkit-scrollbar-thumb {
background: rgba(139, 92, 246, 0.4);
border-radius: 2px;
}
.sdc-scroll-hint {
position: absolute;
bottom: 8px;
left: 50%;
transform: translateX(-50%);
font-size: 0.65rem;
color: rgba(255, 255, 255, 0.3);
z-index: 5;
pointer-events: none;
animation: sdcHintPulse 2s ease-in-out infinite;
}
.sdc-progress-bar {
position: sticky;
top: 0;
left: 0;
width: 100%;
height: 4px;
background: rgba(255, 255, 255, 0.1);
z-index: 10;
}
.sdc-progress-fill {
height: 100%;
background: linear-gradient(90deg, #8b5cf6, #ec4899);
transform-origin: left;
animation: sdcProgressGrow linear;
animation-timeline: scroll(nearest);
transform: scaleX(0);
}
.sdc-progress-content {
padding: 20px;
height: 800px;
display: flex;
flex-direction: column;
gap: 16px;
}
.sdc-progress-content h3 {
font-size: 1rem;
font-weight: 700;
color: #fff;
margin: 0;
}
.sdc-progress-content p {
font-size: 0.78rem;
color: rgba(255, 255, 255, 0.5);
margin: 0;
line-height: 1.6;
}
.sdc-progress-block {
height: 120px;
border-radius: 12px;
background: rgba(255, 255, 255, 0.04);
border: 1px solid rgba(255, 255, 255, 0.08);
}
@keyframes sdcHintPulse {
0%,
100% {
opacity: 0.3;
}
50% {
opacity: 0.7;
}
}
@keyframes sdcProgressGrow {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
Personnaliser
Options passées à l'API ou attributs data-* :
| Option / propriété | Défaut | Effet |
|---|---|---|
Hauteur du cadre (CSS — .sdc-scroll-container et .demo-preview, height: 280px) |
280 px / 280 px | Les deux sont alignées : le cadre extérieur en overflow: hidden coupe tout ce qui dépasse, dont l'indice. Donnez au bloc défilant la hauteur de votre mise en page (flex: 1 dans une carte, height: 60vh…) : la timeline lit la course réelle, rien d'autre à changer. Sans le cadre .demo-preview, remettez box-sizing: border-box : mesuré sans lui, le padding s'ajoute aux 800 px et la course passe de 524 à 560. |
Hauteur du contenu (CSS — .sdc-progress-content, height: 800px) |
800 px (course 524 px) | C'est un gabarit de démonstration : en production, retirez la hauteur fixe (height: auto) et laissez le texte réel faire la course — 0 % en haut, 100 % au dernier pixel, quelle que soit la longueur. Si le contenu ne déborde pas, la timeline reste inactive et la barre vide (mesuré avec 200 px de contenu). |
Piste et remplissage (CSS — .sdc-progress-bar, .sdc-progress-fill) |
4 px, blanc à 10 % / dégradé 90° #8b5cf6 → #ec4899 | L'épaisseur est celle de la piste, le remplissage suit en height: 100%. Le dégradé est comprimé avec le scaleX : la pointe reste rose à toute position ; pour une couleur unie, une seule valeur. Un border-radius sur le remplissage serait lui aussi écrasé à faible échelle — posez-le sur la piste avec overflow: hidden. |
animation-timeline: scroll(nearest) (CSS — .sdc-progress-fill) |
scroll(nearest) — le plus proche ancêtre qui défile, axe du bloc | scroll(root) lit la page entière (mesuré : 0,5 à mi-page, barre en position: fixed) ; scroll(nearest inline) suit un défilement horizontal. Le remplissage doit rester à l'intérieur du cadre : sorti du cadre, nearest remonte au défileur suivant — mesuré, la barre ignore le cadre et suit la page. Pour une barre hors du cadre, nommez la timeline : scroll-timeline-name: --doc sur le cadre, timeline-scope: --doc sur l'ancêtre commun, animation-timeline: --doc sur le remplissage (mesuré : 0,47 à 262 px sur une course de 560). |
animation-range (CSS — absent, soit normal) |
toute la course (0 % → 100 %) | animation-range: 0 50% remplit la barre sur la première moitié seulement — mais, faute d'animation-fill-mode: forwards, elle retombe à zéro passé 50 % (mesuré : 0,5 à 131 px, 0 à 262 et 524). Ajoutez animation-fill-mode: forwards (ou both) dès que la plage ne couvre pas toute la course. |
Courbe et keyframes (CSS — animation: sdcProgressGrow linear, @keyframes sdcProgressGrow) |
linear, from scaleX(0) → to scaleX(1) | Seule courbe qui rende la proportion exacte. Mesuré avec ease-out : 0,68 à mi-course dans Chromium, 0,47 dans WebKit — deux navigateurs, deux barres. Gardez aussi le from { transform: scaleX(0) } : sans lui, WebKit 26.5 rend le carré de la progression (mesuré 0,25 à mi-course), Chromium non. |
Défilement doux (CSS — .sdc-scroll-container, scroll-behavior: smooth) |
smooth | Ne lisse que les défilements programmés et les ancres : mesuré, scrollTop = 262 se relit 0 et arrive en 249 ms dans Chromium, 493 ms dans Firefox ; la molette n'est pas concernée. auto pour un bouton « Aller à l'article 4 » instantané. |
FAQ
Quelle différence avec Scroll Progress Bar (fx-0547) ? Les deux barres se ressemblent trait pour trait.
Le cœur est le même : piste de 4 px en position: sticky, remplissage scaleX(0 → 1) sur animation-timeline: scroll(), dégradé #8b5cf6 → #ec4899, contenu de 800 px dans un cadre de 280. fx-0547 écrit scroll() sans argument — nearest est la valeur par défaut, les deux s'accrochent donc au même défileur. Les différences sont dans le cadre : fx-0547 garde un .demo-preview à min-height: 300px (l'indice se pose sous le cadre), fx-0555 le fige à height: 280px; overflow: hidden (l'indice recouvre le bas du cadre) ; fx-0555 écrit le from des keyframes que WebKit exige, fx-0547 non ; et fx-0555 est gratuit là où fx-0547 est premium. Même famille au catalogue : Progress Reveal (fx-0539) anime une variable --sb-progress sur la même timeline pour remplir un mot en background-clip: text, et Progress Reveal (fx-0588) ajoute sous la même piste collée des cartes révélées par view(). Si vous voulez la valeur (un pourcentage en texte, un bouton débloqué à 100 %) ou Firefox, prenez Introduction (fx-0575), le jumeau en JavaScript.
Ma barre reste vide alors que le cadre défile : pourquoi ?
Trois causes, toutes mesurées. Le navigateur n'a pas animation-timeline — Firefox quelle que soit la version, Chrome avant 115, Safari avant 26 : le remplissage reste à scaleX(0), la piste est vide (masquez-la avec @supports not (animation-timeline: scroll())). Le contenu ne déborde pas : un cadre dont le texte fait 200 px pour 280 de haut a une course nulle, la timeline est attachée mais inactive (currentTime nul) et la barre reste à 0 — elle ne se rabat pas sur la page. Le remplissage a été sorti du cadre : scroll(nearest) cherche alors le défileur le plus proche au-dessus de lui, souvent la page — mesuré, cadre défilé de 262 px, barre à 0 ; page défilée à 700 px, barre à 0,47. Dans ce dernier cas, nommez la timeline (scroll-timeline-name sur le cadre, timeline-scope sur l'ancêtre commun).
Peut-on afficher la progression de la page entière plutôt que celle d'un cadre ?
Oui, en deux changements : animation-timeline: scroll(root) sur .sdc-progress-fill — ou scroll() si aucun ancêtre ne défile — et la piste en position: fixed; top: 0 (ou sticky dans votre en-tête), sans le cadre de 280 px. Mesuré sur une page de 3 000 px : 0 en haut, 0,5 à 1 100 px sur 2 200 de course, 1 en bas. La piste doit rester dans le document, pas dans un autre bloc défilant, sinon nearest le préférerait. Sur mobile, la barre d'adresse change clientHeight et donc la course : la barre se recale toute seule, sans écouteur resize — c'est un avantage du CSS natif sur le jumeau JavaScript.