keeptrack-css
v1.0.2
Published
Read computed CSS property values from elements and expose them as CSS custom properties (variables)
Maintainers
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
- Scrollbar sizes By default the plugin tracks the scrollbar-width of vertical scrollbars (can be disabled) so a
--scrollbar-widthcustom property is available in the document root so you can do things likecalc(100vw - var(--scrollbar-width)). - Anchor linking When you add
data-keeptrack-scroll-paddingto 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. - Keep track of CSS values Keep track of width or height to mimic this on other elements when
display: gridor evensubgridcan't help you out there.
Installation
npm install keeptrack-cssInclude 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-widthis the width (thickness) of the vertical scrollbar--scrollbar-heightis 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-keeptrack — data-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-stuckattribute on the element (for CSS targeting) - A
--[id]-stuckCSS variable on:root(1when stuck,0when not) if the element has anid - A
--stuckCSS variable on the element itself if it has noid
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-keeptrackis dynamically added/removed from elements, or when theirdata-keeptrack-target-parent,data-keeptrack-scroll-padding, oridattributes change - Scroll listener (opt-in via
detectSticky: true) detects whenposition: stickyelements become stuck, using a passive listener throttled withrequestAnimationFrame - 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.
