Introduction
Scroll Progress
A small pill that reports how far the reader has moved through a page or a scroll container. A ring fills as the content advances, and the label names the section you are in.
Why it exists
A sense of position on long pages
Long articles hide their own length. The ring gives readers a continuous cue for how much remains, without adding another block of chrome to the layout.
The signal
One value, mapped to a ring
The component tracks a single scalar between zero and one and maps it to the stroke of the ring. The label, the menu and the placement are presentation on top of that value.
Placement
Pinned to the bottom center
The pill sits at the bottom of the viewport, within reach of the thumb, while the content scrolls beneath it. Pass className to move it, or point it at a specific scroll container.
Interaction
Behavior
The ring follows scroll position directly and the label swaps as each section crosses the offset line. It should feel like a readout of the page rather than an animation that plays on its own.
Tracking
Progress is derived, not stored
Scroll offset divided by the total scrollable distance gives the progress value, so it stays correct through resizes and content changes.
Smoothing
A spring keeps the ring from stuttering
Raw scroll values can arrive in jumps. Passing the value through a spring lets the ring glide toward its target instead of snapping between frames.
Visual System
Styling
The default look is deliberately quiet: a frosted pill with a thin border, tied to the background and foreground tokens so it reads clearly in both themes.
Surface
Frosted, not solid
A translucent background with a backdrop blur keeps the text underneath faintly visible, so the pill feels like part of the page instead of a sticker on top of it.
Contrast
Lean on the theme tokens
The ring, the label and the menu all use the foreground color, so everything inverts correctly when the theme flips between light and dark.
Motion
Sizes spring, labels crossfade
The surface springs between the pill and the menu sizes, while labels crossfade through a light blur so a change of section never feels abrupt.
Implementation
Usage
Drop the component into a scrollable layout and pass the sections you want to name. Everything past that is optional refinement.
Mounting
One instance per scroll surface
Render a single indicator for the page. If you have an independent inner scroll area, give it its own instance and pass that element as containerRef.
Offset
Tune when a section becomes active
The offset sets how far below the top edge a heading has to travel before its label takes over. Raise it for pages with a tall sticky header.
Accessibility
Respect reduced motion
When reduced motion is requested, the surface stops springing, the blur fades are dropped, and jumping to a section scrolls instantly.