Reading CSS Custom Property Values with JavaScript getComputedStyle and setProperty
Read CSS custom property computed values with getComputedStyle and getPropertyValue, write them with setProperty, and register types with @property so JavaScript receives parsed values instead of strings.
You need to read a CSS custom property value from the cascade, not from a stylesheet string. The API is getComputedStyle. It hands back the value after every rule, layer, specificity tie, and inheritance chain has resolved against the element you asked about. That single method is the difference between a design system that stays in sync with its CSS and a JavaScript object that silently drifts the moment someone edits a stylesheet. If you are reading a custom property and getting an empty string where a value clearly exists, this is why: getComputedStyle does not give you the specified value from the author stylesheet; it gives you the resolved value after the cascade has finished. A custom property that references a missing variable, like --theme: var(--undefined), computes to the guaranteed-invalid value, and getPropertyValue returns that as an empty string. That is not a bug. It is the cascade reporting its result.
Reading CSS Custom Property Values with JavaScript getComputedStyle and setProperty
Read a Value from the Cascade
Start with the read side. The method you want is Window.getComputedStyle(), which returns a live CSSStyleDeclaration object. You then call getPropertyValue('--name') on that object. The syntax is one line: window.getComputedStyle(element).getPropertyValue('--custom-prop'). The return value is always a string, even for numeric custom properties. If you registered the property with a numeric syntax via CSS.registerProperty, the resolved value still comes back as a string like '10px', not a JavaScript number. The live nature of the CSSStyleDeclaration means that if the style changes and you read again, you get the new value, but the engine may compute it lazily. Do not expect the value to be correct immediately after a DOM mutation without a forced reflow in some engines.
Avoid the Classic Mistake
The alternative method, element.style.getPropertyValue('--name'), reads only the inline style attribute or values set with element.style.setProperty(). It completely misses custom properties defined in a <style> block, an external stylesheet, or a class rule. That is the classic mistake: reading from element.style when the value lives in the cascade. Use getComputedStyle whenever the value could come from anywhere other than the inline style.
Reading a Colour Custom Property and Passing It to a Canvas fillStyle
Here is a complete, runnable sample that reads a colour custom property and hands it to a canvas fillStyle. The custom property is set on the document root via a class, and the script reads it from an element that inherits it. The value is a string; canvas accepts CSS colour strings directly.
<!DOCTYPE html>
<style>
:root { --brand-colour: #c0392b; }
.card { background: var(--brand-colour); }
</style>
<canvas id="swatch" width="100" height="100"></canvas>
<script>
const card = document.querySelector('.card');
const colour = window.getComputedStyle(card).getPropertyValue('--brand-colour').trim();
const canvas = document.getElementById('swatch');
const ctx = canvas.getContext('2d');
ctx.fillStyle = colour;
ctx.fillRect(0, 0, 100, 100);
</script>
The trim() is necessary because the resolved value may include leading or trailing whitespace. If --brand-colour were not set anywhere, getPropertyValue returns an empty string, and fillStyle would ignore it, leaving the canvas transparent. That is the failure case: the empty string is not a colour, so the draw silently does nothing. Always check for the empty string and fall back to a default.
Writing a Custom Property on an Ancestor to Propagate a Theme Change
Set a Value That Cascades
The write side uses the setProperty method on a CSSStyleDeclaration. You can set a custom property on any element, and because custom properties inherit by default, the change cascades down to all descendants unless a closer rule overrides it. This is the runtime theming technique: flip a value on the root or a container, and the whole subtree recomputes. The method signature is element.style.setProperty('--name', 'value'). It writes to the inline style, which has the highest specificity in the cascade after !important declarations.
Toggle a Theme with setProperty
Here is a complete sample that toggles a theme by changing a custom property on the <body> element.
<!DOCTYPE html>
<style>
body { --theme-bg: #ffffff; --theme-fg: #1a1a1a; }
body.dark { --theme-bg: #1a1a1a; --theme-fg: #ffffff; }
.content { background: var(--theme-bg); color: var(--theme-fg); }
</style>
<div class="content">Hello</div>
<button id="toggle">Toggle Dark</button>
<script>
const content = document.querySelector('.content');
const button = document.getElementById('toggle');
button.addEventListener('click', () => {
const body = document.body;
if (body.classList.contains('dark')) {
body.classList.remove('dark');
body.style.setProperty('--theme-bg', '#ffffff');
body.style.setProperty('--theme-fg', '#1a1a1a');
} else {
body.classList.add('dark');
body.style.setProperty('--theme-bg', '#1a1a1a');
body.style.setProperty('--theme-fg', '#ffffff');
}
});
</script>
The setProperty call overrides the class-based value because inline styles beat class rules. To remove an inline override, use the removeProperty method: body.style.removeProperty('--theme-bg'). That lets the cascade fall back to the stylesheet or inherited value.
Set High Enough in the Tree
Common mistake: setting the property on the element that needs the new value, not on an ancestor. Custom properties inherit, so a value set on a child does not affect siblings or parents. Set it high enough in the tree to cover the affected subtree. Also, be careful with calc() in the value; the string you pass must be valid CSS. A trailing semicolon inside the value is an error.
Registering a Custom Property with @property So JavaScript Reads a Typed Value
If you want getComputedStyle to return a number instead of a string, or to enforce a type, use the CSS Properties and Values API. The @property at-rule in CSS and the CSS.registerProperty() method in JavaScript both register a custom property with a syntax descriptor. When registered, the resolved value is parsed according to that syntax, and getPropertyValue returns a string that matches the registered type, such as a length, a colour, or an integer. But there is a catch: CSS.registerProperty() ships in Chrome, Edge, and Safari 16.4. Firefox does not ship it; check caniuse before relying on it. The @property at-rule has broader support but still varies. Here is a sample that registers a custom property with a length syntax and reads it back.
<!DOCTYPE html>
<style>
@property --spacing {
syntax: '<length>';
inherits: true;
initial-value: 0px;
}
.box { padding: var(--spacing); }
</style>
<div class="box" style="--spacing: 24px">Content</div>
<script>
const box = document.querySelector('.box');
const spacing = window.getComputedStyle(box).getPropertyValue('--spacing');
console.log(spacing);
</script>
Even with registration, the return is a string. To use it as a number, parse it with parseFloat(spacing). The real benefit: the browser validates the value against the syntax and rejects invalid assignments, so --spacing: red becomes the initial value 0px, and getPropertyValue returns '0px'. Without registration, an invalid value is treated as unset, and the var() fallback kicks in. The CSS.registerProperty() method is useful for dynamic registration, but the @property at-rule is declarative and does not require JavaScript. Check CSS.supports('--custom', 'value') to detect support before using either.
The Computed Value Pipeline and Why getComputedStyle Returns What It Returns
The reason getComputedStyle returns the resolved value rather than the cascaded declaration lies in the CSS Cascading and Inheritance Level 4 specification. That specification defines three stages: the specified value, the computed value, and the used value. The specified value is what the cascade produces after origin, layer, specificity, and source order have been applied. The computed value is the specified value after resolving relative units, var() references, and other computations. The used value is the computed value after layout-dependent resolution, such as percentages against a containing block. getComputedStyle returns the computed value, not the specified value and not the used value. For most custom properties, the computed value is the specified value as a string, but with var() references, the browser resolves them during computation. If a custom property references a missing variable, like --x: var(--undefined), the computed value is the guaranteed-invalid value, which serialises to an empty string. That is why you get '' from getPropertyValue, and why you must check for it. The cascade is a constraint solver; a custom property’s value is the output of that solver for the element in question, not the input from a stylesheet. This is the core difference from preprocessor variables: a Sass $variable is compiled into static CSS at build time, but a custom property is a live, cascading, inheriting value that changes at runtime.
The Failure Case: Custom Property Not Updating, and How to Diagnose It
Check the Inheritance Chain
When a custom property does not update, the first suspect is the inheritance chain. Set --theme on a parent but read from an element that is not a descendant? The value is not inherited. Custom properties inherit by default, but only down the tree.
Watch for Specificity and !important
The second suspect is a specificity battle. Inline styles have higher specificity than any selector, but a !important declaration in a stylesheet beats an inline style without !important.
Diagnose Syntax and Timing
The third suspect is a typo in the var() fallback syntax. var(--missing, fallback) works, but var(--missing fallback) is invalid and the whole declaration is ignored. The fourth suspect is timing: getComputedStyle may return a stale value immediately after a style change because the engine computes lazily. Force a reflow by reading element.offsetHeight or use requestAnimationFrame. The fifth suspect is reading from element.style instead of getComputedStyle; element.style only sees inline values, so a stylesheet value is invisible. The last resort: check CSS.supports('--custom', 'value') to see if the engine supports custom properties at all. Every modern engine does, but older device-locked browsers may not. The common mistake is expecting getPropertyValue to return a number; it always returns a string. Compare '10px', not 10.
Design Tokens: Replacing the Duplicated JavaScript Object
The technique this replaces is storing design-token values in a JavaScript object. A typical old pattern: a tokens.js file exporting { colorPrimary: '#c0392b' }, and a CSS file with :root { --color-primary: #c0392b; }. Two copies of the same value. The moment someone changes the CSS variable but not the JavaScript object, or vice versa, the canvas, a charting library, or a WebGL shader uses the stale value. That drift is the failure mode. The correct pattern: read the value from the cascade via getComputedStyle, or write the value to the cascade via setProperty. Let CSS be the single source of truth. This is what design systems mean when they say custom properties are the runtime bridge between CSS and JavaScript. The cascade is the constraint solver; JavaScript reads the solved result. This eliminates the duplicate-definition problem entirely. For a theme that changes at runtime, set a custom property on an ancestor and let inheritance propagate. The JavaScript side never stores the colour; it reads it when needed. If a designer changes the stylesheet, the JavaScript picks up the new value without a code change.
Practical Guidance: When to Use getComputedStyle vs element.style, and Fallback Patterns
Choose the Right Method
Use getComputedStyle when the value could come from any stylesheet, a class, an inherited parent, or a @property registration. Use element.style only for values you set inline yourself in the same script, and only if you need to avoid triggering a full style recalc. The getComputedStyle call is relatively expensive; do not call it in a tight loop inside an animation frame. Read once, store the value, and only re-read when you know the style changed.
Fallback Patterns
The fallback pattern for reading is to check the empty string: const value = style.getPropertyValue('--prop') || 'fallback'. The fallback pattern for writing is to test support: if (CSS.supports('--custom', 'value')) { /* set it */ }. This guards against older engines that do not support custom properties. The @supports (--custom: value) guard works in CSS. For registered properties, the CSS.registerProperty() interaction means the resolved value type is enforced, so getPropertyValue returns a string that matches the syntax; parse it with parseFloat or parseInt for numbers. The removeProperty method is the inverse of setProperty; it deletes the inline value and lets the cascade fall back. The CSSStyleDeclaration returned by getComputedStyle is live, but the value may be computed lazily. Force a reflow if you need the value immediately after a DOM change. The specified value from the stylesheet is not what you get; the computed value is what you get. That distinction is the whole game.
What This Replaces in the Older Toolbox
Before custom properties shipped, reading a CSS value from JavaScript meant parsing a stylesheet manually, reading element.style.color, or using data attributes. element.style.color only works for inline styles. Reading a stylesheet required iterating document.styleSheets and matching selectors. That was brittle and slow. Data attributes meant storing a value twice: once in data-* and once in CSS. Custom properties collapse all of that. Preprocessor variables from Sass and Less are compiled away at build time; they do not exist at runtime. You cannot read a $variable from JavaScript. Custom properties exist at runtime and are readable. The gap in support is narrow: Chrome 49, Firefox 31, Safari 9.1. Treat CSS as the declarative constraint solver and JavaScript as the reader of its output. This is the difference between a static build step and a live system. When you write a custom property with setProperty, you are not setting a string in a stylesheet. You are instructing the cascade to compute a new value for that property on that element, and the browser handles the style recalculation, the inheritance, and the paint. The var() function is the consumer; getComputedStyle is the observer.
The One Thing to Do Next
Open your browser’s developer tools on a page that uses custom properties, select an element, and in the console run getComputedStyle(element).getPropertyValue('--your-variable'). If it returns an empty string, trace the inheritance chain and the cascade. If it returns a value, change it with element.style.setProperty('--your-variable', 'new-value') and watch the page update. That exercise will teach you more than any tutorial.