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

vugluscr

v2.0.0

Published

A custom scrollbar with markers and a minimap, for a page or any inner scroller

Readme

Vugluscr

Vugluscr replaces the browser's scrollbar with one that tells you more: a track you can click anywhere to jump to, coloured markers showing where things of interest sit in the document, and an optional minimap of the content's block structure — the affordances IDEs and text editors have had for years.

It takes over the page, or any inner scroller, and several instances coexist happily on one page. No framework, no dependencies.

Try it → — a page rail and an embedded one on the same page, with markers you can toggle on.

Install

npm install vugluscr

Use it with a bundler

import { Scrollbar } from "vugluscr";
import "vugluscr/theme.css"; // optional — the stock look

const rail = new Scrollbar();

That much takes over the page. Markers are pushed in as pixel offsets down the content — the library never searches the DOM for what to mark, so what counts as interesting is entirely yours:

function paintMarkers() {
  const contentTop = document.body.getBoundingClientRect().top;
  rail.setMarkers(
    Array.from(document.querySelectorAll("mark")).map((element) => {
      const rect = element.getBoundingClientRect();
      return { start: rect.top - contentTop, end: rect.bottom - contentTop, color: "crimson" };
    }),
  );
}

/* Offsets move whenever geometry does, so recompute rather than cache. */
rail.onLayout(paintMarkers);

Use it as vanilla JS

One file, styles included. No bundler, no stylesheet link — from a CDN:

<script src="https://unpkg.com/vugluscr@2"></script>
<script>
  var rail = new Vugluscr.Scrollbar();
</script>

jsDelivr serves the same file from https://cdn.jsdelivr.net/npm/vugluscr@2. Both resolve a bare vugluscr to the latest release, which is fine for a try-out and a liability in production — pin at least the major, as above.

Or from the installed package, at node_modules/vugluscr/dist/vugluscr.standalone.js. That build is an IIFE meant for a <script> tag: it sets the global and nothing else, so importing it through a bundler gets you the side effect, not the exports. A bundler should use the main entry.

The global is the library, not the class — a bare Scrollbar global would be an easy collision on someone else's page.

An inner scroller

Pass the element that owns the scroll position. The rail forces overflow: auto and min-block-size: 0 on it so it actually clips and scrolls, and makes the container a positioning context — both reverted on dispose().

const rail = new Scrollbar({
  scrollContainer: document.getElementById("panel"),
  contentElement: document.getElementById("panelContent"),
});

contentElement must be a descendant of scrollContainer — the content that moves with scrolling, not the scroller itself. Marker offsets are measured from it, and it is what the minimap draws.

Options

| Option | Default | | | ----------------- | ------------------------------------------------------ | ----------------------------------------------------------- | | scrollContainer | the page | Element that owns the scroll position. | | container | the scroller's parent, or document.body | Where the rail mounts. | | contentElement | the scroller's first element child, or document.body | Marker origin and minimap source. | | showMinimap | true | Draw the minimap pane beside the track. | | autoHide | true | Hide the rail while the content fits, and while fullscreen. | | minimap | | { minimapWidth, showThumb: 'always' \| 'mouseover' } | | track | | { width, cursorColor, minMarkerHeight } |

Methods

  • setMarkers(markers) — replaces the marker set. Each is { start, end?, color, lane? }, where start / end are pixel offsets down contentElement and lane is 'left' | 'center' | 'right' | 'full' (default 'full') selecting which third of the track width to draw in. Very short markers are clamped to a minimum height so a single-line hit stays visible, and markers are batched by colour when painted.
  • setCursorOffset(offset) — a current-position indicator on the track; negative hides it.
  • onLayout(listener) — fires after each layout pass, once geometry has settled. Returns something with a dispose() method.
  • dispose() — removes the rail and restores everything it changed.

ScrollRail is also exported: the rendered widget without the mount layer, for a host that already owns its scroll state. Scrollbar is that plus the DOM and scroll-source plumbing.

There is no IDisposable to implement: teardown is structural. Every subscription hands back something carrying a dispose() method, and disposing a rail releases everything under it.

Styling

Two kinds of CSS, kept apart on purpose.

Layout is the library's. The positioning, the pointer-events routing, the clipping, and the suppression of the native scrollbar are what make the thing work — change them and you don't restyle the scrollbar, you break it. So they are not shipped as a file to import: Scrollbar injects them into the document itself, once, as <style data-vugluscr="structure"> in @layer vugluscr.structure. Nothing to import, nothing to load in the right order, nothing to forget.

Looks are yours. theme.css holds only what is safe to change — colours, fills, transitions, cursors — in @layer vugluscr.theme inside @scope (.vugluscr). Import it for the stock appearance or skip it and style .vugluscr from scratch. Either way the scrollbar still works.

Retheming

Everything neutral is a tint of one colour, so matching the rail to a design is usually a single declaration:

:root {
  --vugluscr_color: rebeccapurple;
}

The tints, faintest to strongest:

| | | | --------------- | ------------------------------------------------- | | 11% | track and minimap backing | | 17% | minimap containers (section, figure, ul, …) | | 23% | minimap leaf blocks (p, li, figcaption, …) | | 25% | minimap blocks of unrecognised elements | | 30% · 50% · 60% | thumb · hovered · dragging | | 90% | markers |

--vugluscr_accent_color is the exception: images in the minimap read as a different kind of content rather than a louder tint of the same one, so they carry their own colour.

Every derived value is also a token in its own right, so one thing can break ranks without giving up the knob:

:root {
  --vugluscr_color: rebeccapurple; /* the whole rail */
  --vugluscr_thumb_background: tomato; /* except the thumb */
}

--vugluscr_track_background, --vugluscr_thumb_background (_hover, _active), --vugluscr_minimap_background, --vugluscr_minimap_thumb_background, --vugluscr_minimap_block (_group, _unknown, _image, _stroke), --vugluscr_marker_color and --vugluscr_marker_min_height.

Tokens are read but never declared: a declaration on .vugluscr inside the theme would sit closer to the element than your :root and quietly win, so the defaults live in var() fallbacks at the point of use.

Markers, which CSS cannot otherwise reach

Markers are painted onto a canvas, so a stylesheet cannot touch them the way it touches an element. ScrollbarTrack reads two values back out of its own computed style at paint time so their prominence stays a styling decision:

  • --vugluscr_marker_min_height — a single-position marker is drawn at exactly this height, which makes it the knob for how loudly occurrences read on the rail.
  • --vugluscr_marker_color — for markers that carry no colour of their own.

The colour rides on the track's color property rather than being read raw, so whatever you write is resolved by the browser first — color-mix(), a chain of variables, currentColor — before the canvas, which cannot parse any of that, ever sees it. A colour passed with an individual marker still wins over the token, which is how a consumer with several kinds of marker keeps them apart.

Overriding selectors

Because the theme is layered, any unlayered rule of yours wins without !important:

.vugluscr .track .thumb {
  background-color: rebeccapurple;
}

What you can target:

.vugluscr [.is_embedded]
└── .scrollbar
    ├── .minimap
    │   ├── svg > rect.<element-name>   one per content block
    │   └── .thumb                      viewport indicator
    └── .track
        ├── canvas                      markers
        └── .thumb [.active]            draggable

The rail also sets vugluscr_active on <html> (page mode) or vugluscr_embedded on the scroller, which is how the native scrollbar gets hidden. Both are removed on dispose().

State is set by the library, rendered by the theme: showThumb: 'mouseover' toggles visible on the minimap's indicator from a pointer listener rather than a :hover rule, so restyling the theme changes how the indicator looks, never whether it appears.

Development

npm run dev       # test rig at / — page and embedded rails side by side
npm test          # unit tests
npm run test:e2e  # Playwright, against the rig
npm run build     # both package outputs into dist/
npm run build:demo # the rig as a static site, into demo-dist/

The demo is the rig, built from src/ and deployed to GitHub Pages by .github/workflows/pages.yml on every push to main.

The e2e suite is where the real behaviour is pinned — mount modes, scroll driving, wheel forwarding, auto-hide, and the CSS-driven marker prominence above. It runs against test-rig/, which mounts both a page-mode and an embedded scrollbar on one page and exposes them on window.vugluscrRig so a test can push exact marker offsets instead of inferring them from content.

test-rig/standalone.html exercises the vanilla build. Run npm run build first, and open it through a plain static server rather than npm run dev — Vite's dev server runs the IIFE bundle through its module transform, which rewrites the global away.

How it is put together

| | | | -------------------- | -------------------------------------------------------------------------------- | | Scrollable | scroll state and smooth-scroll animation; no DOM | | ScrollHost | the scroll source — the window, or an element | | CachedDomNode | write-caching wrapper so repeated style writes don't hit the DOM | | ScrollTrackOverlay | shared base for anything drawn on the rail: click-anywhere, drag-continue, thumb | | ScrollbarTrack | the canvas marker track | | Minimap | the SVG block map | | ScrollRail | composes track + optional minimap over one Scrollable | | Scrollbar | mounts a ScrollRail on a scroll source and keeps the two in sync | | structure.ts | the load-bearing CSS, injected once |

Scrollbar is what a consumer wants. ScrollRail is exported for a host that already owns its scroll state and only needs the rendered widget; its domNode is the rail's root element, to place or measure. Everything else in the table is internal.

Teardown is structural throughout: internally, anything with a dispose() method can be registered with a store, and every subscription returns one. The store and the emitter behind that are not exported — what a consumer needs is the dispose() method it was handed, not the machinery under it.

Credits

The scroll engine, the write-caching DOM wrapper, the canvas marker track and the minimap are adapted from VS Code's editor internals (scrollable.ts, fastDomNode.ts, decorationsOverviewRuler, the minimap). MIT, like this package — see LICENSE for the terms and NOTICE for the attribution.