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

keeptrack-css

v1.0.2

Published

Read computed CSS property values from elements and expose them as CSS custom properties (variables)

Readme

KeepTrack

KeepTrack reads computed CSS property values from elements and exposes them as CSS custom properties (variables). This lets you use values like an element's rendered height or background-color elsewhere in your CSS — something not normally possible.

It automatically updates when elements resize, when the DOM changes, or (optionally) on every animation frame for non-layout properties.

Use-cases

  1. Scrollbar sizes By default the plugin tracks the scrollbar-width of vertical scrollbars (can be disabled) so a --scrollbar-width custom property is available in the document root so you can do things like calc(100vw - var(--scrollbar-width)).
  2. Anchor linking When you add data-keeptrack-scroll-padding to one or more sticky or fixed elements, anchor links will take this into account when jumping to that anchor. Also works on pageload and using back/forward navigation.
  3. Keep track of CSS values Keep track of width or height to mimic this on other elements when display: grid or even subgrid can't help you out there.

Installation

npm install keeptrack-css

Include via a <script> tag, CommonJS, or ES modules:

<script src="keepTrack.min.js"></script>
// CommonJS
const KeepTrack = require('keeptrack-css');

// ES modules
import KeepTrack from 'keeptrack-css';

Basic usage

const tracker = new KeepTrack();

Add data-keeptrack to any HTML element with a comma-separated list of CSS properties to track:

<div data-keeptrack="height">...</div>

This sets --height as an inline CSS variable on the element itself, updated whenever the element resizes.

You can track multiple properties:

<div data-keeptrack="height, width, padding-top">...</div>

Where the CSS variable is set

The target for the CSS variable depends on the element's attributes:

On the element itself (default)

<!-- Input -->
<div data-keeptrack="height">...</div>

<!-- Result -->
<div data-keeptrack="height" style="--height: 64px">...</div>

Multiple properties:

<!-- Input -->
<div data-keeptrack="height, width, padding-top">...</div>

<!-- Result -->
<div data-keeptrack="height, width, padding-top" style="--height: 64px; --width: 320px; --padding-top: 16px">...</div>

On the document root (via id)

If the element has an id, the variable is set on :root with the id as a prefix:

<!-- Input -->
<header id="site-header" data-keeptrack="height">...</header>

<!-- Result: sets --site-header-height on :root -->
<html style="--site-header-height: 80px">
  ...
  <header id="site-header" data-keeptrack="height">...</header>
  ...
</html>
main {
  padding-top: var(--site-header-height);
}

On a target parent (via data-keeptrack-target-parent)

You can set the variable on a parent or any other element. The attribute accepts either a number (levels to traverse up) or a CSS selector:

<!-- Traverse 2 levels up -->
<!-- Input -->
<div class="grandparent">
  <div class="parent">
    <div data-keeptrack="height" data-keeptrack-target-parent="2">...</div>
  </div>
</div>

<!-- Result: --height is set on .grandparent -->
<div class="grandparent" style="--height: 64px">
  <div class="parent">
    <div data-keeptrack="height" data-keeptrack-target-parent="2">...</div>
  </div>
</div>
<!-- Closest ancestor matching the selector -->
<!-- Input -->
<div class="wrapper">
  <div>
    <div data-keeptrack="height" data-keeptrack-target-parent=".wrapper">...</div>
  </div>
</div>

<!-- Result: --height is set on .wrapper -->
<div class="wrapper" style="--height: 64px">
  <div>
    <div data-keeptrack="height" data-keeptrack-target-parent=".wrapper">...</div>
  </div>
</div>

When using a selector, KeepTrack first tries el.closest(selector) to find the nearest ancestor. If no ancestor matches, it falls back to document.querySelector(selector).

If the element also has an id, the variable name includes the id:

<!-- Input -->
<div class="layout">
  <div id="sidebar" data-keeptrack="width" data-keeptrack-target-parent=".layout">...</div>
</div>

<!-- Result: --sidebar-width is set on .layout -->
<div class="layout" style="--sidebar-width: 250px">
  <div id="sidebar" data-keeptrack="width" data-keeptrack-target-parent=".layout">...</div>
</div>

Scrollbar dimensions

By default, KeepTrack sets --scrollbar-width on :root, updated on viewport resize. You can also enable --scrollbar-height.

  • --scrollbar-width is the width (thickness) of the vertical scrollbar
  • --scrollbar-height is the height (thickness) of the horizontal scrollbar
new KeepTrack({
  scrollbarWidth: true,   // default: true
  scrollbarHeight: true   // default: false
});
.full-width {
  width: calc(100vw - var(--scrollbar-width));
}

Scroll padding

Add data-keeptrack-scroll-padding to any element to automatically set scroll-padding-top on :root. This fixes anchor links (<a href="#section">) being hidden behind sticky headers. The element does not need data-keeptrackdata-keeptrack-scroll-padding works on its own.

<!-- Input -->
<header id="site-header" data-keeptrack="height" data-keeptrack-scroll-padding>
  ...
</header>
<main>
  <section id="about">...</section>
</main>

<!-- Result: scroll-padding-top is set on :root to the header's height -->
<html style="--site-header-height: 80px; scroll-padding-top: 80px">
  ...
</html>

If multiple elements have data-keeptrack-scroll-padding, their heights are summed:

<header data-keeptrack="height" data-keeptrack-scroll-padding>...</header>
<nav data-keeptrack="height" data-keeptrack-scroll-padding>...</nav>
<!-- scroll-padding-top = header height + nav height -->

When detectSticky is enabled, only elements that are currently stuck contribute to scroll-padding-top. When clicking an anchor link, KeepTrack predicts which sticky elements will be stuck at the target position and adjusts scroll-padding-top before the browser scrolls. This ensures correct scroll offsets even when a sticky element's container ends before the anchor target.

Sticky detection

Enable detectSticky to detect when position: sticky elements become stuck. KeepTrack checks on scroll and exposes the state as:

  • A data-keeptrack-stuck attribute on the element (for CSS targeting)
  • A --[id]-stuck CSS variable on :root (1 when stuck, 0 when not) if the element has an id
  • A --stuck CSS variable on the element itself if it has no id
new KeepTrack({ detectSticky: true });
<!-- Input -->
<header id="site-header" data-keeptrack="height" style="position: sticky; top: 0">
  ...
</header>

<!-- Result when stuck -->
<html style="--site-header-height: 80px; --site-header-stuck: 1">
  ...
  <header id="site-header" data-keeptrack="height" data-keeptrack-stuck style="position: sticky; top: 0">
    ...
  </header>
  ...
</html>
/* Style changes when stuck */
[data-keeptrack-stuck] {
  box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}

The onChange callback also fires for sticky state changes with prop set to "stuck":

new KeepTrack({
  detectSticky: true,
  onChange(el, prop, value) {
    if (prop === 'stuck') {
      console.log(el, value === '1' ? 'is stuck' : 'is not stuck');
    }
  }
});

Sticky top resolution (calc/var) and caching

KeepTrack resolves sticky element top values to pixels for sticky detection and anchor prediction. It supports px, em, rem, %, and complex values like calc(...) and var(...).

Resolved top values are cached and only recomputed on resize, DOM mutations, or recalculate(). This caching only affects sticky detection and anchor prediction. If your top value actually changes during scroll (rare), you can opt into per-frame updates:

new KeepTrack({ stickyTopDynamic: true });

Options

new KeepTrack({
  scrollbarWidth: true,   // Track scrollbar width as --scrollbar-width on :root
  scrollbarHeight: false, // Track scrollbar height as --scrollbar-height on :root
  debounceTime: 250,      // Debounce delay in ms for resize and DOM changes
  poll: false,            // Enable requestAnimationFrame polling for non-layout changes
  detectSticky: false,    // Detect when sticky elements become stuck
  stickyTopDynamic: false, // Update sticky top values every frame
  onChange: null           // Callback when a tracked value changes
});

poll

Enable this to track properties that don't affect element size, like background-color, color, or font-size. When enabled, KeepTrack checks all tracked values every animation frame and only updates when a value has changed.

new KeepTrack({ poll: true });

If the browser doesn't support ResizeObserver, enable poll to keep values in sync with size changes.

detectSticky

Enable this to detect when position: sticky elements are stuck. Uses a passive scroll listener throttled with requestAnimationFrame for minimal performance impact.

new KeepTrack({ detectSticky: true });

stickyTopDynamic

When false (default), KeepTrack caches resolved sticky top values for performance. This only affects sticky detection and anchor prediction. Set to true if your top value changes during scroll.

new KeepTrack({ stickyTopDynamic: true });

onChange

Called whenever a tracked value changes (including sticky state). Receives the element, the property name, and the new value:

new KeepTrack({
  onChange(el, prop, value) {
    console.log(`${prop} changed to ${value}`, el);
  }
});

API

init(options)

Re-initializes with new options. Cleans up the previous instance first.

tracker.init({ poll: true });

destroy()

Removes all event listeners, observers, and stops polling. Also cleans up all CSS variables, scroll-padding-top, and data-keeptrack-stuck attributes set by KeepTrack.

tracker.destroy();

recalculate()

Manually trigger a recalculation of all tracked elements and scrollbar dimensions.

tracker.recalculate();

observe(element)

Programmatically start tracking an element (must have a data-keeptrack attribute):

tracker.observe(document.querySelector('.my-element'));

unobserve(element)

Stop tracking an element, remove its CSS variables, and clean up its caches:

tracker.unobserve(document.querySelector('.my-element'));

How it works

KeepTrack uses multiple mechanisms to detect changes:

  • ResizeObserver tracks size changes on individual [data-keeptrack] elements
  • MutationObserver detects when tracked elements are added/removed from the DOM, when data-keeptrack is dynamically added/removed from elements, or when their data-keeptrack-target-parent, data-keeptrack-scroll-padding, or id attributes change
  • Scroll listener (opt-in via detectSticky: true) detects when position: sticky elements become stuck, using a passive listener throttled with requestAnimationFrame
  • requestAnimationFrame polling (opt-in via poll: true) catches computed style changes that don't affect element size, like color or font changes

All paths use a value cache to avoid unnecessary setProperty calls when nothing has changed. Calling destroy() or unobserve() fully cleans up any CSS variables and attributes that were set.