scroll-trace
v0.1.1
Published
A tiny, dependency-free scroll progress indicator for static websites.
Maintainers
Readme
scroll-trace
A tiny, dependency-free scroll progress indicator for static websites.
ScrollTrace tracks the document or a native scrollable element, mounts at any edge, and keeps visual customization in CSS. It automatically recalculates when content is added or the tracked layout changes.
Install
pnpm add scroll-tracenpm install scroll-traceyarn add scroll-traceCDN
<script type="module">
import ScrollTrace from "https://cdn.jsdelivr.net/npm/scroll-trace/dist/index.js";
const trace = new ScrollTrace();
</script>Quick start
import ScrollTrace from "scroll-trace";
const trace = new ScrollTrace();The default instance tracks the page and creates a full-width indicator at the top of the viewport.
Placement
const trace = new ScrollTrace({
placement: "right",
});Supported placements:
type ScrollTracePlacement = "top" | "right" | "bottom" | "left";Top and bottom indicators fill horizontally. Left and right indicators fill vertically.
Scrollable elements
Pass an element or selector as target:
const panelTrace = new ScrollTrace({
target: ".scroll-panel",
placement: "right",
className: "panel-progress",
});When mount is omitted, an element target also receives the generated indicator.
Separate target and mount
The target is measured. The mount receives the generated DOM:
const panelTrace = new ScrollTrace({
target: ".scroll-panel",
mount: ".panel-shell",
placement: "right",
className: "panel-progress",
});Selectors must resolve when the instance starts. Element references can be used instead:
const panelTrace = new ScrollTrace({
target: document.querySelector<HTMLElement>(".scroll-panel")!,
mount: document.querySelector<HTMLElement>(".panel-shell")!,
});The page mount uses fixed positioning. Other mount elements use absolute positioning.
When a non-page mount is statically positioned, ScrollTrace temporarily gives it
position: relative and restores the previous inline value after the final instance using
that mount is destroyed.
CSS customization
ScrollTrace injects its small set of structural defaults once per document. No CSS import is required. Visual customization belongs to your stylesheet.
.scroll-trace {
--scroll-trace-color: #635bff;
--scroll-trace-track-color: transparent;
--scroll-trace-size: 5px;
--scroll-trace-radius: 0;
--scroll-trace-z-index: 9999;
}Use className for independently styled instances:
const pageTrace = new ScrollTrace({
className: "page-progress",
});
const sidebarTrace = new ScrollTrace({
target: ".sidebar",
placement: "left",
className: "sidebar-progress",
});.page-progress {
--scroll-trace-color: #111;
--scroll-trace-size: 2px;
}
.sidebar-progress {
--scroll-trace-color: #ef4444;
--scroll-trace-track-color: rgb(0 0 0 / 8%);
--scroll-trace-size: 4px;
--scroll-trace-radius: 999px;
}For complete control, style the generated root and bar:
.gradient-progress {
--scroll-trace-size: 4px;
}
.gradient-progress [data-scroll-trace-bar] {
background: linear-gradient(90deg, #7c3aed, #ec4899);
}Internal behavior uses data attributes, so changing or removing the default class does not break the instance:
<div
class="gradient-progress"
data-scroll-trace
data-scroll-trace-placement="top"
data-scroll-trace-scope="viewport"
>
<span data-scroll-trace-bar></span>
</div>The live normalized value is also exposed as --scroll-trace-progress on the root.
Dynamic content
ScrollTrace watches relevant geometry and child-list changes. Content appended by a load-more button automatically changes the measured scroll range.
It also refreshes after:
- tracked element or document resizing;
- viewport resizing;
- images and other resources loading inside the target.
For an unusual layout change that does not resize an observed box or add/remove DOM nodes, refresh manually:
trace.refresh();Hide the native scrollbar
Optionally hide the target's native scrollbar while preserving wheel, touch, keyboard, and programmatic scrolling:
const panelTrace = new ScrollTrace({
target: ".scroll-panel",
hideNativeScrollbar: true,
});The option is false by default. ScrollTrace applies browser-specific scrollbar rules
to the resolved target and restores its previous state after stop(), update(), or
destroy(). Shared targets remain hidden until the final owning instance releases them.
Hide when there is no scroll
An indicator can stay out of the layout until its target has a scrollable range:
const panelTrace = new ScrollTrace({
target: ".scroll-panel",
hideWhenNoScroll: true,
});The option is false by default. Resize and content observers automatically reveal or
hide the indicator as the available scroll range changes.
Progress callback
Use the existing optimized render cycle instead of adding another scroll listener:
const trace = new ScrollTrace({
onProgress(progress) {
output.textContent = `${Math.round(progress * 100)}%`;
},
});The callback runs after the initial measurement and whenever the normalized progress
changes. Its argument is always between 0 and 1.
Multiple instances
Create one instance per progress indicator:
const pageTrace = new ScrollTrace();
const navigationTrace = new ScrollTrace({
target: ".navigation-list",
placement: "right",
className: "navigation-progress",
});
const galleryTrace = new ScrollTrace({
target: ".gallery",
mount: ".gallery-shell",
placement: "bottom",
className: "gallery-progress",
});Instances share one base style element but own their listeners, observers, generated elements, and progress state independently.
API
Options
| Option | Type | Default | Description |
| --- | --- | --- | --- |
| target | Document \| HTMLElement \| string | document | Scroll source to measure |
| mount | HTMLElement \| string | Target or document.body | Parent for the indicator |
| placement | "top" \| "right" \| "bottom" \| "left" | "top" | Mount edge |
| className | string | "scroll-trace" | Classes assigned to the generated root |
| hideNativeScrollbar | boolean | false | Visually hide the target's native scrollbar |
| hideWhenNoScroll | boolean | false | Hide the indicator when the target cannot scroll |
| onProgress | (progress: number) => void | — | Receive initial and changed normalized progress |
Properties
trace.element; // HTMLDivElement | null
trace.progress; // number from 0 to 1
trace.active; // boolean
trace.destroyed; // booleanMethods
trace.start();
trace.stop();
trace.refresh();
trace.update({
placement: "left",
className: "alternate-progress",
});
trace.destroy();start()mounts and begins tracking. Repeated calls are safe.stop()removes generated DOM and listeners. The instance can be started again.refresh()immediately recalculates progress and returns the normalized value.update()applies behavioral options and remounts an active instance.destroy()permanently cleans up the instance. Repeated calls are safe.
Performance
- no runtime dependencies;
- passive scroll and resize listeners;
- at most one scheduled animation frame per instance;
- transform-based visual updates;
- resize observation for geometry changes;
- child-list observation for dynamic content;
- complete observer, frame, listener, DOM, style, and mount cleanup.
Development
pnpm install
pnpm dev
pnpm checkpnpm dev opens the interactive demo. pnpm check runs type checking, tests, the
package build, and the production demo build.
