Skip to content

Instantly share code, notes, and snippets.

@arenagroove
Last active April 27, 2025 06:44
Show Gist options
  • Select an option

  • Save arenagroove/d7abfa8b7f94da8895d035b6172fd799 to your computer and use it in GitHub Desktop.

Select an option

Save arenagroove/d7abfa8b7f94da8895d035b6172fd799 to your computer and use it in GitHub Desktop.
JavaScript utility for reading and parsing CSS custom properties from elements. Supports unit-aware numeric parsing (px, em, rem, %, vw, etc.) and returns raw or computed values. Includes support for calc() and var() chains.

CSSUtils – CSS Variable Inspector Utility

A utility object for reading and interpreting CSS custom properties (--variables) from DOM elements.

Features

  • ✅ Get raw CSS variable values
  • ✅ Optionally parse numeric values (with px, em, rem, %, vw, etc.)
  • ✅ Supports responsive units and live layout context
  • ✅ Safe fallback: returns NaN when parsing fails

Usage

const el = document.querySelector("#my-box");

const raw = CSSUtils.getCssVariableValue(el, "--custom-size");            // "2em"
const parsed = CSSUtils.getCssVariableValue(el, "--custom-size", true);  // 32 (if base font-size is 16px)

CodePen Demo

CodePen Example

const CSSUtils = {
/**
* Reads the value of a CSS custom property (--variable) from an element.
*
* @param {HTMLElement} el - The target element
* @param {string} varName - The CSS variable name (e.g., "--custom-size")
* @param {boolean} parseAsNumber - If true, attempts to convert the value to a number in pixels
* @returns {string|number} - Raw string or numeric pixel value (or NaN if parse fails)
*/
getCssVariableValue(el, varName, parseAsNumber = false) {
const computedStyle = getComputedStyle(el);
const value = computedStyle.getPropertyValue(varName)?.trim() || "";
if (!parseAsNumber) return value;
const match = value.match(/^([\d.]+)(px|%|em|rem|vw|vh|vmin|vmax)?$/i);
if (!match) return NaN;
const numericValue = parseFloat(match[1]);
const unit = (match[2] || "px").toLowerCase();
switch (unit) {
case "em":
return numericValue * parseFloat(computedStyle.fontSize);
case "rem":
return numericValue * parseFloat(getComputedStyle(document.documentElement).fontSize);
case "%": {
const parent = el.parentElement;
if (!parent) return NaN;
const isWidthContext = true; // Assumes width context
const parentSize = isWidthContext ? parent.offsetWidth : parent.offsetHeight;
return (numericValue / 100) * parentSize;
}
case "vw":
return (numericValue / 100) * window.innerWidth;
case "vh":
return (numericValue / 100) * window.innerHeight;
case "vmin":
return (numericValue / 100) * Math.min(window.innerWidth, window.innerHeight);
case "vmax":
return (numericValue / 100) * Math.max(window.innerWidth, window.innerHeight);
default:
return numericValue;
}
}
};
@arenagroove
Copy link
Author

arenagroove commented Apr 27, 2025

CSSUtils.getCssVarValue V2

Method Purpose Example
CSSUtils.getCssVarValue(el, varName) Read raw CSS variable value (string). const raw = CSSUtils.getCssVarValue(box, "--padding");
CSSUtils.getCssVarValue(el, varName, true) Read and parse numeric value (live px, %, vw, etc.). const num = CSSUtils.getCssVarValue(box, "--padding", true);
.once(el, varName, parseAsNumber?) Read once, no observer, no live updates. const size = CSSUtils.getCssVarValue.once(box, "--padding", true);
.getter(el, varName, parseAsNumber?, options, dynamic?) Create dynamic getter (default) or static snapshot. const getter = CSSUtils.getCssVarValue.getter(box, "--padding");
const value = getter();
.getter(..., false) Return static value immediately (no function). const value = CSSUtils.getCssVarValue.getter(box, "--padding", true, {}, false);
.batch(el, [varNames], parseAsNumbers?) Read multiple variables efficiently at once. const [w, h] = CSSUtils.getCssVarValue.batch(box, ["--width", "--height"], true);
.cleanup() Disconnect all observers and reset caches. CSSUtils.getCssVarValue.cleanup();

Best Practices

Scenario Recommended API
Static one-time value .once() or .getter(..., false)
Live dynamic updates needed .getter() (default dynamic mode)
Reading many variables together .batch()
Cleaning on page transitions .cleanup()

Quick Notes

  • .getter() with dynamic = true creates a live fresh reader.
  • .getter(..., false) or .once() is faster, no observers, static only.
  • ResizeObserver auto-invalidates viewport units (vw, vh, vmin, vmax).
  • MutationObserver auto-invalidates inline style changes.
  • Uses CSS Typed OM parsing when available for performance boost.

CodePen Demo

CodePen Example

const CSSUtils = {
        getCssVarValue: (() => {
            // ---------- Configurations ----------
            const MAX_CACHE_SIZE = 1000;
            const VALUE_REGEX = /^([\d.]+)([a-z%]+)?$/i;

            // ---------- Internal Cache ----------
            const cache = {
                rootFontSize: +getComputedStyle(document.documentElement).fontSize.match(VALUE_REGEX)[1],
                viewportSizes: {
                    vw: window.innerWidth,
                    vh: window.innerHeight,
                    vmin: Math.min(window.innerWidth, window.innerHeight),
                    vmax: Math.max(window.innerWidth, window.innerHeight)
                },
                elementData: new WeakMap(),
                lastWindowSize: { width: window.innerWidth, height: window.innerHeight },
            };

            let resizeObserver = null;
            let mutationObserver = null;
            let typedOMParser = null;

            // ---------- Try TypedOM parser if available ----------
            try {
                if (window.CSS?.number) {
                    typedOMParser = value => {
                        try {
                            const num = CSS.number(value);
                            return { numeric: num.value, unit: num.unit || 'px' };
                        } catch {
                            return null;
                        }
                    };
                }
            } catch { /* ignore if unsupported */ }

            // ---------- Handle window resize ----------
            const handleResize = () => {
                requestAnimationFrame(() => {
                    const nw = window.innerWidth;
                    const nh = window.innerHeight;
                    if (nw !== cache.lastWindowSize.width || nh !== cache.lastWindowSize.height) {
                        cache.viewportSizes = {
                            vw: nw,
                            vh: nh,
                            vmin: Math.min(nw, nh),
                            vmax: Math.max(nw, nh)
                        };
                        cache.lastWindowSize = { width: nw, height: nh };
                        cache.rootFontSize = +getComputedStyle(document.documentElement).fontSize.match(VALUE_REGEX)[1];
                    }
                });
            };

            // ---------- Handle mutations (style attribute) ----------
            const handleMutations = mutations => {
                for (const m of mutations) {
                    if (m.attributeName === 'style') {
                        cache.elementData.delete(m.target);
                    }
                }
            };

            // ---------- Setup observers ----------
            const initObservers = () => {
                if (!resizeObserver) {
                    resizeObserver = new ResizeObserver(handleResize);
                    resizeObserver.observe(document.documentElement);
                }
                if (!mutationObserver) {
                    mutationObserver = new MutationObserver(handleMutations);
                    mutationObserver.observe(document.documentElement, { attributes: true, subtree: true, attributeFilter: ['style'] });
                }
            };

            // ---------- Cleanup (if needed) ----------
            const cleanup = () => {
                resizeObserver?.disconnect();
                mutationObserver?.disconnect();
                cache.elementData = new WeakMap();
            };

            // ---------- Parse value using TypedOM or fallback ----------
            const parseValue = value => {
                const parsed = typedOMParser?.(value);
                if (parsed) return parsed;
                const match = VALUE_REGEX.exec(value);
                return match ? { numeric: +match[1], unit: (match[2] || 'px').toLowerCase() } : { numeric: NaN, unit: '' };
            };

            // ---------- Detect if a value is a complex expression ----------
            const isExpression = value => /^(clamp|min|max|calc)\(/i.test(value.trim());

            const getValue = (el, varName, parseAsNumber = false, options = {}) => {
                if (!el || !varName) return parseAsNumber ? NaN : '';

                const observe = options.observe !== false;
                const decimals = options.decimals;

                let computedValue;
                if (observe) {
                    initObservers();
                    let elData = cache.elementData.get(el);
                    if (!elData) {
                        elData = {};
                        cache.elementData.set(el, elData);
                    }
                    computedValue = elData[varName] || (elData[varName] = getComputedStyle(el).getPropertyValue(varName).trim());
                } else {
                    computedValue = getComputedStyle(el).getPropertyValue(varName).trim();
                }

                if (!parseAsNumber) return computedValue;

                const { numeric, unit } = parseValue(computedValue);
                if (isNaN(numeric)) return NaN;

                let result;
                switch (unit) {
                    case 'em':
                        result = numeric * +getComputedStyle(el).fontSize.match(VALUE_REGEX)[1];
                        break;
                    case 'rem':
                        result = numeric * cache.rootFontSize;
                        break;
                    case '%': {
                        const parent = el.parentElement;
                        result = parent ? numeric * parent.offsetWidth * 0.01 : NaN;
                        break;
                    }
                    case 'vw':
                        result = numeric * cache.viewportSizes.vw * 0.01;
                        break;
                    case 'vh':
                        result = numeric * cache.viewportSizes.vh * 0.01;
                        break;
                    case 'vmin':
                        result = numeric * cache.viewportSizes.vmin * 0.01;
                        break;
                    case 'vmax':
                        result = numeric * cache.viewportSizes.vmax * 0.01;
                        break;
                    default:
                        result = numeric;
                }

                if (typeof decimals === 'number') {
                    const factor = 10 ** decimals;
                    result = Math.round(result * factor) / factor;
                }

                return result;
            };

            // ---------- Batch Getter ----------
            getValue.batch = (el, varNames, parseAsNumbers = false, options = {}) => {
                if (!el || !Array.isArray(varNames)) return [];

                const parseAll = !Array.isArray(parseAsNumbers) && parseAsNumbers;
                const results = new Array(varNames.length);

                for (let i = 0; i < varNames.length; i++) {
                    results[i] = getValue(el, varNames[i], parseAll || parseAsNumbers[i], options);
                }
                return results;
            };

            // ---------- Helpers ----------
            getValue.once = (el, varName, parseAsNumber = false, options = {}) =>
                getValue(el, varName, parseAsNumber, { ...options, observe: false });

            getValue.cleanup = cleanup;

            // ---------- Helper: Resolved Color Caching ----------
            let colorResolverEl = null;

            getValue.resolveColor = (el, varName) => {
                if (!colorResolverEl) {
                    colorResolverEl = document.createElement('div');
                    colorResolverEl.style.cssText = `all: initial;position: absolute;width: 0;height: 0;overflow: hidden;pointer-events: none;visibility: hidden;`;
                    document.body.appendChild(colorResolverEl);
                }
                const rawValue = getComputedStyle(el).getPropertyValue(varName).trim();
                if (!rawValue) return '';
                colorResolverEl.style.cssText += `--resolved-color: ${rawValue};background-color: var(--resolved-color);`;
                return getComputedStyle(colorResolverEl).backgroundColor;
            };

            getValue.getter = (el, varName, parseAsNumber = false, options = {}, dynamic = true) => {
                return dynamic ?
                    () => getValue(el, varName, parseAsNumber, options) :
                    getValue(el, varName, parseAsNumber, { ...options, observe: false });
            };

            getValue.isExpression = isExpression;

            initObservers();

            return getValue;
        })()
    };

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment