Stedefast

Plugins

Reading Progress

3 min read

/plugin-reading-progress injects a thin progress bar fixed to the top of the viewport. The bar fills as the user scrolls through the content element, giving readers a visual indication of how far through an article they are.

The entire implementation is a small inline vanilla JS island — no framework dependency and no extra HTTP request. Total injected size is under 500 bytes gzipped.

Installation

pnpm add /plugin-reading-progress

Basic setup

// stedefast.config.ts
import { defineConfig } from "@stedefast/core";
import { ReadingProgressPlugin } from "/plugin-reading-progress";

export default defineConfig({
  // ...
  plugins: [
    ReadingProgressPlugin(),
  ],
});

With custom options:

ReadingProgressPlugin({
  color: "#3b82f6",
  height: 3,
  contentSelector: "article",
})

Options reference

Option Type Default Description
color string "#3b82f6" Progress bar fill colour (any CSS colour value)
height number 3 Bar height in pixels
contentSelector string "article" CSS selector for the element whose scroll position is tracked
applyToTypes string[] all pages Content types to show the bar on (see note below)

How it works

The plugin injects an inline <style> block and an inline <script> block into every page <head> via headAssets.

On page load the script:

  1. Creates a <div id="sf-progress"> element and appends it to <body>.
  2. Queries the content element using contentSelector.
  3. Attaches a passive scroll listener that calls requestAnimationFrame to throttle updates.
  4. On each animation frame, reads getBoundingClientRect() on the content element and computes the scroll fraction as -rect.top / (rect.height - window.innerHeight), clamped to [0, 1].
  5. Sets el.style.width to the fraction as a percentage string — no animation library needed.

The bar element is positioned fixed at top: 0; left: 0; z-index: 9999 with pointer-events: none so it never interferes with clicks.

prefers-reduced-motion support

The injected CSS includes a @media (prefers-reduced-motion: reduce) block that removes the transition from #sf-progress. When a user has requested reduced motion, the bar jumps directly to position instead of sliding:

@media (prefers-reduced-motion: reduce) {
  #sf-progress { transition: none; }
}

CSS customisation

The progress bar element has the id sf-progress. You can override any of its styles in your theme's global.css:

/* theme/styles/global.css */

/* Gradient bar instead of a solid colour */
#sf-progress {
  background: linear-gradient(to right, #3b82f6, #8b5cf6);
}

/* Thicker bar */
#sf-progress {
  height: 5px;
}

/* Add a glow */
#sf-progress {
  box-shadow: 0 0 8px #3b82f6;
}

Your stylesheet overrides are applied after the plugin's injected <style> block, so standard CSS specificity rules apply — id selectors in your own stylesheet will win over the injected styles.

Note on applyToTypes

The applyToTypes option appears in the type definition and is accepted by the plugin constructor, but per-type filtering is not yet implemented. If you set this option, the plugin will log a warning at build time and inject the progress bar on all pages regardless.

Per-type filtering (injecting the bar only on pages whose content type matches the list) is planned for a future release. Track progress on the Stedefast issue tracker.