Accessing Every CSS Custom Property From JavaScript
Reading a single CSS custom property’s value in JavaScript is straightforward. Once you’ve declared a property like --color-accent on an element, you can retrieve it with getComputedStyle and getPropertyValue:
const colorAccent = getComputedStyle(document.documentElement)
.getPropertyValue('--color-accent'); // #00eb9b
That pattern works well for one-off lookups, and it even stays in sync when the CSS value changes. But it becomes repetitive when you need to read many custom properties at once. Each new property requires its own explicit call:
const colorAccent = getComputedStyle(document.documentElement).getPropertyValue('--color-accent'); // #00eb9b
const colorAccentSecondary = getComputedStyle(document.documentElement).getPropertyValue('--color-accent-secondary'); // #9db4ff
const colorAccentTertiary = getComputedStyle(document.documentElement).getPropertyValue('--color-accent-tertiary'); // #f2c0ea
const colorText = getComputedStyle(document.documentElement).getPropertyValue('--color-text'); // #292929
const colorDivider = getComputedStyle(document.documentElement).getPropertyValue('--color-text'); // #d7d7d7
You can reduce duplication with a small helper function:
const getCSSProp = (element, propName) => getComputedStyle(element).getPropertyValue(propName);
const colorAccent = getCSSProp(document.documentElement, '--color-accent'); // #00eb9b
// repeat for each custom property...
Still, every custom property added to the stylesheet requires a matching line in JavaScript. That’s manageable for a few properties but doesn’t scale. This article walks through an automated approach: collect every custom property from all same-domain stylesheets on a page and render them into a color palette. The full working demo is available on CodePen.
Outline of the Process
- Get all stylesheets on the page, both external and internal
- Filter out stylesheets hosted on third-party domains
- Extract all CSS rules from the remaining stylesheets
- Keep only basic style rules
- Collect every property name and value from those rules
- Remove non-custom properties
- Build HTML that displays the results as color swatches
Step 1: Gather Stylesheets
All page stylesheets are available through the global document.styleSheets object, which is array-like. Converting it to a real array lets us use standard array methods. We’ll wrap this in a reusable function:
const getCSSCustomPropIndex = () => [...document.styleSheets];
Invoking getCSSCustomPropIndex returns an array of CSSStyleSheet objects, one for every external and internal stylesheet on the current page:

Step 2: Keep Only Same-Domain Stylesheets
Accessing a stylesheet from a different domain is blocked by browsers. Per the MDN documentation, accessing cssRules on a cross-origin stylesheet throws a SecurityError. This technique therefore only inspects stylesheets hosted on the current domain.
Each CSSStyleSheet object has an href property containing its full URL, or null for internal styles. Comparing that value against location.origin filters out third-party files:
const isSameDomain = (styleSheet) => {
if (!styleSheet.href) {
return true;
}
return styleSheet.href.indexOf(window.location.origin) === 0;
};
Applying this filter to the stylesheet array leaves only the stylesheets we can safely inspect:
const getCSSCustomPropIndex = () => [...document.styleSheets]
.filter(isSameDomain);
Step 3: Extract All CSS Rules
The next step is to flatten every rule from every stylesheet into a single array. Using reduce and concat combines the per-sheet cssRules lists into one flat structure:
const getCSSCustomPropIndex = () => [...document.styleSheets]
.filter(isSameDomain)
.reduce((finalArr, sheet) => finalArr.concat(...sheet.cssRules), []);
The spread operator unpacks each CSSStyleSheet.cssRules collection. What remains is a one-dimensional array of CSSRule objects:

Step 4: Keep Only Style Rules
CSS defines several rule types via numeric constants on the CSSRule interface. The most common is CSSStyleRule, but @media queries, @supports, @font-face, and @keyframes produce other types. Custom properties may legitimately appear inside @media blocks, but keeping this demo focused on plain style rules avoids the extra traversal logic those nested rules would require.
Every rule exposes a type property. A filter that checks for CSSStyleRule removes everything else:
const isStyleRule = (rule) => rule.type === 1;
It’s used like this:
const getCSSCustomPropIndex = () => [...document.styleSheets]
.filter(isSameDomain)
.reduce((finalArr, sheet) => finalArr.concat(
[...sheet.cssRules].filter(isStyleRule)
), []);
Step 5: Collect All Properties
Each CSSStyleRule contains a style object — a CSSStyleDeclaration — listing every property in that rule, both standard and custom. Using another reduce, we can collect the name and value of every property from every rule into an array of arrays:
const getCSSCustomPropIndex = () => [...document.styleSheets]
.filter(isSameDomain)
.reduce((finalArr, sheet) => finalArr.concat(
[...sheet.cssRules]
.filter(isStyleRule)
.reduce((propValArr, rule) => {
const props = [...rule.style].map((propName) => [
propName.trim(),
rule.style.getPropertyValue(propName).trim()
]);
return [...propValArr, ...props];
}, [])
), []);
The rule.style collection is array-like, so spreading it allows iteration with map. For each property name, getPropertyValue returns the associated value. Both name and value are trimmed to remove stray whitespace.
The result is a flat array where each child array holds a property name and its value:

This includes standard CSS properties like color and font-family, so one more filter is required.
Step 6: Isolate Custom Properties
Custom property names always begin with two dashes (--). Checking that prefix is a reliable filter:
([propName]) => propName.indexOf("--") === 0)
Applied to each property/name pair via the props array:
const getCSSCustomPropIndex = () =>
[...document.styleSheets].filter(isSameDomain).reduce(
(finalArr, sheet) =>
finalArr.concat(
[...sheet.cssRules].filter(isStyleRule).reduce((propValArr, rule) => {
const props = [...rule.style]
.map((propName) => [
propName.trim(),
rule.style.getPropertyValue(propName).trim()
])
.filter(([propName]) => propName.indexOf("--") === 0);
return [...propValArr, ...props];
}, [])
),
[]
);
Inside the filter callback, array destructuring (([propName])) accesses the first element of each child array, and indexOf confirms the -- prefix. The output is a clean index of every custom property, with name and value:

There is a potential future simplification: the CSS Typed Object Model Level 1 draft defines CSSStyleRule.styleMap, which would expose properties in a format that skip the manual map step:
// ...
const props = [...rule.styleMap.entries()].filter(/*same filter*/);
// ...
At present, only Chrome and Edge support styleMap, and the spec is still a draft, so it’s not used for this demo. Once the data structure is finalized, the custom-property index itself still serves the same purpose.
Rendering the Swatches
With the data assembled, the remaining work is DOM generation. Store the result of getCSSCustomPropIndex, then fill an empty list with one item per color:
document.querySelector(".colors").innerHTML = cssCustomPropIndex.reduce(
(str, [prop, val]) => `${str}<li class="color">
<b class="color__swatch" style="--color: ${val}"></b>
<div class="color__details">
<input value="${prop}" readonly />
<input value="${val}" readonly />
</div>
</li>`,
"");
Inside the reduce callback, destructuring again picks apart each [prop, val] pair. Each list item gets a b element whose inline style assigns the custom property as the swatch’s background color:
<b class="color__swatch" style="--color: ${val}"></b>
The generated HTML looks like this:
<b class="color__swatch" style="--color: #00eb9b"></b>
Each swatch’s --color value comes from the inline style, and the external stylesheet applies it to background-color. The palette renders automatically from whatever custom properties exist in the stylesheets.
The same approach applies to any reusable CSS value — spacing scales, font stacks, breakpoint tokens — anything defined as a custom property can populate a pattern library page without hand-written JavaScript for each entry.



