Reading Progress
stimeo--reading-progress
Hands CSS how far through an article you have read, so the progress bar is pure CSS.
Measures how far through an article you have read and hands that fraction to CSS. It writes it both on the article and on the page as a whole, so a bar fixed anywhere can fill from CSS alone. The usual visibility machinery cannot express this, because the value stays flat while a tall article passes through the screen, so the maths lives here instead. The work is paced to the browser's own drawing, so scrolling stays smooth, and an article inside a scrolling box works too. Reaching the end is reported. Mark a decorative bar as hidden from screen readers.
Scroll this page: the bar above the article fills from the CSS value alone, with no JavaScript of your own.
Paragraph 1: scroll on — the bar above tracks how far through this article you are, computed from the article's geometry and published as a CSS custom property.
Paragraph 2: scroll on — the bar above tracks how far through this article you are, computed from the article's geometry and published as a CSS custom property.
Paragraph 3: scroll on — the bar above tracks how far through this article you are, computed from the article's geometry and published as a CSS custom property.
Paragraph 4: scroll on — the bar above tracks how far through this article you are, computed from the article's geometry and published as a CSS custom property.
Paragraph 5: scroll on — the bar above tracks how far through this article you are, computed from the article's geometry and published as a CSS custom property.
Paragraph 6: scroll on — the bar above tracks how far through this article you are, computed from the article's geometry and published as a CSS custom property.
Paragraph 7: scroll on — the bar above tracks how far through this article you are, computed from the article's geometry and published as a CSS custom property.
Paragraph 8: scroll on — the bar above tracks how far through this article you are, computed from the article's geometry and published as a CSS custom property.
Paragraph 9: scroll on — the bar above tracks how far through this article you are, computed from the article's geometry and published as a CSS custom property.
Paragraph 10: scroll on — the bar above tracks how far through this article you are, computed from the article's geometry and published as a CSS custom property.
Paragraph 11: scroll on — the bar above tracks how far through this article you are, computed from the article's geometry and published as a CSS custom property.
Paragraph 12: scroll on — the bar above tracks how far through this article you are, computed from the article's geometry and published as a CSS custom property.
Paragraph 13: scroll on — the bar above tracks how far through this article you are, computed from the article's geometry and published as a CSS custom property.
Paragraph 14: scroll on — the bar above tracks how far through this article you are, computed from the article's geometry and published as a CSS custom property.
Paragraph 15: scroll on — the bar above tracks how far through this article you are, computed from the article's geometry and published as a CSS custom property.
Paragraph 16: scroll on — the bar above tracks how far through this article you are, computed from the article's geometry and published as a CSS custom property.
Paragraph 17: scroll on — the bar above tracks how far through this article you are, computed from the article's geometry and published as a CSS custom property.
Paragraph 18: scroll on — the bar above tracks how far through this article you are, computed from the article's geometry and published as a CSS custom property.
Paragraph 19: scroll on — the bar above tracks how far through this article you are, computed from the article's geometry and published as a CSS custom property.
Paragraph 20: scroll on — the bar above tracks how far through this article you are, computed from the article's geometry and published as a CSS custom property.
Paragraph 21: scroll on — the bar above tracks how far through this article you are, computed from the article's geometry and published as a CSS custom property.
Paragraph 22: scroll on — the bar above tracks how far through this article you are, computed from the article's geometry and published as a CSS custom property.
Paragraph 23: scroll on — the bar above tracks how far through this article you are, computed from the article's geometry and published as a CSS custom property.
Paragraph 24: scroll on — the bar above tracks how far through this article you are, computed from the article's geometry and published as a CSS custom property.
Paragraph 25: scroll on — the bar above tracks how far through this article you are, computed from the article's geometry and published as a CSS custom property.
Paragraph 26: scroll on — the bar above tracks how far through this article you are, computed from the article's geometry and published as a CSS custom property.
Paragraph 27: scroll on — the bar above tracks how far through this article you are, computed from the article's geometry and published as a CSS custom property.
Paragraph 28: scroll on — the bar above tracks how far through this article you are, computed from the article's geometry and published as a CSS custom property.
Keyboard
This component has no keyboard interactions of its own.
<%# reading-progress: the article publishes its progress as a CSS custom property
on itself AND on :root; the bar here consumes it with pure CSS (no consumer
JS). Scroll the page to move the bar. %>
<div class="reading-progress-demo">
<p class="reading-progress-demo__hint"><%= t("components.reading_progress.demo.hint") %></p>
<div class="reading-progress-demo__bar" aria-hidden="true"></div>
<article class="reading-progress-demo__article" data-controller="stimeo--reading-progress">
<% 28.times do |i| %>
<p><%= t("components.reading_progress.demo.paragraph", index: i + 1) %></p>
<% end %>
</article>
</div>
/*
* Presentation-only styles for the reading-progress demo. The library maintains
* --stimeo--reading-progress (0..1); the bar's width is pure CSS from that
* variable. The bar is sticky so it stays visible while the article scrolls.
*/
.reading-progress-demo__hint {
margin: 0 0 0.75rem;
color: var(--muted);
}
.reading-progress-demo__bar {
position: sticky;
/* Clear the site header: pinned at 0 the bar sits behind it, so the reader sees
nothing move even though the progress value is changing. */
top: var(--header-h, 3.5rem);
z-index: 31;
height: 0.375rem;
border-radius: 0.25rem;
background: var(--border);
overflow: hidden;
}
.reading-progress-demo__bar::before {
content: "";
display: block;
height: 100%;
width: calc(var(--stimeo--reading-progress, 0) * 100%);
background: var(--accent);
}
/*
* The article must be decisively taller than the viewport: progress is
* -top / (height - viewportHeight), so an article shorter than the viewport is
* binary (0 -> 1) by contract and the bar never fills gradually. Real text plus
* a vh floor keeps the demo progressive on any screen size.
*
* The floor also has to be tall enough to dominate the page it sits on: with only
* a little more than one viewport of slack, the bar reads 0 through the page header
* and is already full long before the footer, which looks broken rather than
* gradual. The paragraphs carry the article past two viewports on their own.
*
* The paragraph count is what actually fills those three viewports — the floor is a
* safety net for very tall screens, not the thing holding the article open. Too few
* paragraphs and the article ends in a wide blank band that reads as a rendering
* fault rather than as an article.
*/
.reading-progress-demo__article {
margin-top: 0.75rem;
min-height: 200vh;
}
.reading-progress-demo__article p {
margin: 0 0 1.5rem;
color: var(--fg);
}
This demo needs no consumer-side JS (the controller handles the behavior).
These demo styles use shared design tokens (light + dark). Copy the shared styles too, then toggle data-theme on your root element for dark mode.
The data-* attributes you add to your own HTML to wire this component. Put the data-controller below on a root element, then place its targets / values / actions inside that element.
On the root element
data-controller="stimeo--reading-progress"
Events
| Name | Description | Event |
|---|---|---|
change
|
Fires with { progress } as the progress moves (rAF granularity). |
stimeo--reading-progress:change |
complete
|
Fires once the progress reaches 1. | stimeo--reading-progress:complete |
State hooks
The library only manages these ARIA/data attributes and custom properties. Your CSS reads them to render the look — selectors like [aria-selected], [aria-expanded], or var(--stimeo--…) hook into this state.
| Hook | Target | Meaning |
|---|---|---|
--stimeo--reading-progress |
Controller element + document root (CSS custom property) | The reading progress, 0..1 (the root copy is removed on disconnect). |