npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

scroll-trace

v0.1.1

Published

A tiny, dependency-free scroll progress indicator for static websites.

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-trace
npm install scroll-trace
yarn add scroll-trace

CDN

<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; // boolean

Methods

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 check

pnpm dev opens the interactive demo. pnpm check runs type checking, tests, the package build, and the production demo build.