CSS Custom Property Not Working — Scope, Typos, Cycles, and JavaScript Fixes
Quick answer
💡CSS custom properties fail silently when the property is not declared on an ancestor in scope, when the name has a typo (custom property names are case-sensitive: --myColor and --mycolor are different), when a circular reference exists between two properties, or when you try to use var() inside a media query condition. Check the Computed tab in DevTools and search for the property name to find its resolved value.
Error symptoms
- ✕
var(--my-color) resolves to the browser default instead of the declared value - ✕
A component's CSS custom property works in isolation but fails inside the design system - ✕
Custom property declared in JavaScript via setProperty has no visible effect - ✕
var() inside a media query condition is ignored entirely - ✕
Changing a custom property in DevTools does not update the element's computed style - ✕
Custom property works in Chrome but resolves differently in Safari 14
Common causes
- •Property declared on a non-ancestor element so the child cannot inherit it
- •Typo in the property name: --myColor and --mycolor are different properties
- •Circular reference: --a: var(--b); --b: var(--a) — both resolve to the guaranteed-invalid value
- •Property contains an invalid value for the context where it is used, such as a string used as a length
- •var() used in a media query condition where custom properties cannot be resolved
- •JavaScript calling setProperty with a leading space in the property name or value
When it happens
- •After moving a component into a Shadow DOM tree where host-level variables are not visible
- •When a CSS preprocessor converts a custom property name to different casing in the compiled output
- •After a design token rename where some references were updated and others were missed
- •When using a dark mode toggle that calls setProperty on document.documentElement but the element reads the property from a different ancestor
- •When @property registration uses a syntax type that rejects the actual value being assigned
Examples and fixes
A custom property declared on a sibling or unrelated element cannot be inherited by another element.
Scope problem: property declared on wrong element
❌ Wrong
/* Property declared on .sidebar, not an ancestor of .card */
.sidebar {
--card-bg: #f8fafc;
}
.card {
background: var(--card-bg); /* resolves to nothing */
border-radius: 8px;
padding: 1rem;
}✅ Fixed
/* Declare on :root for global scope */
:root {
--card-bg: #f8fafc;
}
.card {
background: var(--card-bg);
border-radius: 8px;
padding: 1rem;
}
/* Or declare on the parent wrapper */
.card-section {
--card-bg: #f8fafc;
}
.card-section .card {
background: var(--card-bg);
}CSS custom properties are inherited — they flow down the DOM tree from parent to child, just like font-size or color. They do not flow sideways between siblings or up from child to parent. When --card-bg is declared on .sidebar and .card is a sibling or unrelated element, .card cannot see that custom property and var(--card-bg) resolves to the guaranteed-invalid value, which acts as if the declaration does not exist. Moving the declaration to :root makes it available globally. Alternatively, declare it on the nearest common ancestor of any element that needs to use it.
Custom property names are case-sensitive: --myColor and --mycolor are two different properties.
Case-sensitive name typo
❌ Wrong
:root {
--primaryColor: #6366f1;
--BorderRadius: 8px;
}
.button {
background: var(--primarycolor); /* lowercase c — different property */
border-radius: var(--borderradius); /* all lowercase — different property */
color: white;
}✅ Fixed
:root {
--primary-color: #6366f1;
--border-radius: 8px;
}
.button {
background: var(--primary-color);
border-radius: var(--border-radius);
color: white;
}Unlike regular CSS properties (background-color and BACKGROUND-COLOR are the same), custom property names are matched case-sensitively by the browser. --primaryColor and --primarycolor are entirely separate properties. The browser does not warn about an undefined custom property — var() just returns the guaranteed-invalid value. The fix renames all properties to use all-lowercase kebab-case, which is the conventional naming scheme for CSS custom properties and eliminates case-related confusion. Consistent casing also makes it easier to search the codebase for property usages.
When two custom properties reference each other, both resolve to the guaranteed-invalid value.
Circular reference between two custom properties
❌ Wrong
:root {
--gap: calc(var(--spacing) + 4px);
--spacing: calc(var(--gap) - 4px);
}
.layout {
gap: var(--gap);
padding: var(--spacing);
}✅ Fixed
:root {
--base-spacing: 16px;
--gap: calc(var(--base-spacing) + 4px); /* 20px */
--spacing: calc(var(--base-spacing) - 4px); /* 12px */
}
.layout {
gap: var(--gap);
padding: var(--spacing);
}When --gap references --spacing and --spacing references --gap, the browser detects the cycle at computed value time and marks both properties as the guaranteed-invalid value. Neither gap nor padding will receive any value from these declarations. The fix introduces a base anchor value --base-spacing that neither --gap nor --spacing depends on. Both properties now derive their value from the same stable base without referencing each other. This pattern — one base value, multiple derived tokens — is the correct architecture for design token systems.
How custom property inheritance actually works
CSS custom properties participate in the normal CSS inheritance cascade, which means they flow down the DOM tree from ancestor to descendant. A property declared on :root is visible to every element in the document. A property declared on .container is visible only to .container and its descendants. A property declared on a sibling, a cousin, or an unrelated element is completely invisible to the element trying to use it via var().
This inheritance model is what makes custom properties useful for theming — you declare all your design tokens on :root and every element in the page can access them. But it is also the most common cause of silent failure: developers declare a property on an element that has no descendant relationship with the element consuming it.
The case-sensitivity rule catches many developers off guard because regular CSS property names are case-insensitive. --myColor, --MyColor, and --mycolor are three distinct custom properties. If you declare --accentColor on :root and use var(--accentcolor) on a button, the var() resolves to the guaranteed-invalid value with no warning. The browser's Computed tab will show an empty value for the property.
Custom properties also interact with the CSS cascade in an unusual way when it comes to invalid values. If you declare --size: red on :root and then use width: var(--size), the browser resolves the custom property at computed time and finds that red is not a valid length. The declaration becomes invalid at computed value time — which is different from a parse-time error. The result is that the element uses the inherited value of width (usually auto) rather than the initial value, which can be surprising.
Finding a broken custom property in DevTools
Open DevTools, select the element, and open the Computed tab. Type the custom property name in the filter box. If the property appears with a value, it is defined in scope and the issue is likely a typo in the consuming rule. If it does not appear, the property is not defined on any ancestor of this element.
To trace the cascade, open the Styles tab and look at all the rules that match the element. The Styles panel shows inherited properties if you scroll down or enable the Show all option. Alternatively, use the console: getComputedStyle(document.querySelector('.my-element')).getPropertyValue('--my-color'). This returns the resolved value as a string, or an empty string if the property is undefined or has an empty value.
For JavaScript-set properties, use the Sources panel to add a breakpoint in the code that calls setProperty. Verify the property name has exactly two leading dashes and no leading whitespace. A common mistake is calling element.style.setProperty('- -color', value) with a space between the dashes, which creates a property named - -color rather than --color. After setting the property, run getComputedStyle(element).getPropertyValue('--color') in the console to confirm it resolved.
For circular reference issues, the Computed tab will show both properties as empty even though they appear in the Styles panel. Temporarily replace one property with a literal value — set --spacing: 16px instead of calc(var(--gap) - 4px) — and check if the other property resolves. If it does, you have confirmed a circular dependency.
The DevTools filter search in the Styles panel also lets you find all usages of a specific variable across every rule that applies to the selected element. Type the variable name in the filter field at the top of the Styles tab and every rule containing that name is highlighted, making it fast to spot where a property is declared, overridden, or referenced without scrolling through hundreds of rules manually.
Fixing the most common custom property failures
For scope issues, move the property declaration to the nearest common ancestor of all elements that need it. If a property needs to be available everywhere, declare it on :root. If it is specific to a component, declare it on the component's root element and use it only in that component's descendants. This keeps the property scoped and avoids polluting the global namespace.
For typos, adopt a consistent naming convention and enforce it. The most widely used convention is all-lowercase kebab-case: --primary-color, --border-radius, --font-size-lg. Avoid camelCase and PascalCase for custom property names because they create case-sensitivity risks. If you use a design token tool like Style Dictionary or Theo, configure it to output all property names in kebab-case.
For circular references, identify the cycle by drawing out the dependency graph. Every custom property should ultimately resolve to a literal value — a color hex code, a pixel value, a unitless number. A property that references another property that references another is fine as long as there is no loop. If you find a loop, introduce an anchor property with a literal value that breaks the cycle.
For the var()-in-media-query limitation, use a CSS preprocessor variable instead of a custom property. In Sass, $breakpoint-md: 768px can be used as @media (min-width: #{$breakpoint-md}), and the preprocessor resolves it to a literal value at build time. Container Queries are an alternative that does not require a breakpoint at all — @container (min-width: 400px) targets the container's width rather than the viewport, making them a better fit for reusable components.
Edge cases: @property, Shadow DOM, and JavaScript
The @property at-rule allows you to register a custom property with a specific syntax type, an initial value, and an inheritance setting. @property --border-radius { syntax: "<length>"; inherits: false; initial-value: 4px; } means --border-radius will not inherit from parent elements and defaults to 4px if unset. If the value assigned to a registered property does not match the syntax descriptor, the browser uses the initial-value instead of the guaranteed-invalid value, which can make it harder to notice an assignment error.
Shadow DOM creates a boundary for custom property inheritance. Custom properties declared on the host element are visible inside the Shadow DOM, but properties declared inside the Shadow DOM are not visible outside it. This is intentional and is the mechanism that allows web components to expose a theming API through custom properties while encapsulating their internal implementation. If your component's custom properties are not working inside a Shadow DOM, check whether the property is being declared on the host or passed through the host's CSS custom property API.
When setting custom properties via JavaScript, there are two methods with different scopes. element.style.setProperty('--color', '#f00') sets an inline style that is scoped to that element and its descendants, with the highest specificity. document.documentElement.style.setProperty('--color', '#f00') sets it on the html element, making it globally available as a high-specificity override. Use getComputedStyle(element).getPropertyValue('--color').trim() to read the resolved value — the trim() call removes any leading whitespace that the browser may include in the returned string.
On iOS Safari, custom properties inside @keyframes have historically had inconsistent behavior. An animation that changes a custom property value at different keyframe percentages may not interpolate correctly in Safari older than version 15.4. If you need to animate a custom property value (for example, changing --hue across keyframes), use @property with the correct syntax type, which enables the browser to interpolate the typed value rather than treating it as an opaque string.
Common mistakes with custom properties
Declaring custom properties inside a media query block works but does not mean the property is available only inside that breakpoint. The property is still inherited. What changes inside the media query is the value: @media (max-width: 768px) { :root { --sidebar-width: 0px; } } changes the value of --sidebar-width at that breakpoint. This is the correct pattern for responsive design tokens. The mistake is thinking the property itself is conditionally defined — it is always available, but its value changes.
Using an empty string as a custom property value is not the same as deleting it. element.style.setProperty('--color', '') sets the property to an empty string, which is a valid (non-empty) custom property value — specifically, it is a whitespace-only value. Any var(--color) reference will resolve to an empty string, not to the inherited value. To remove an inline custom property and allow inheritance to resume, use element.style.removeProperty('--color').
Nesting custom property references too deeply creates maintenance problems. A chain like --button-primary uses var(--brand-action), which uses var(--color-indigo-600), which uses var(--color-indigo-base), is hard to trace. Keep chains to at most two or three levels. Use a tool like Token Studio or Style Dictionary to manage the token hierarchy and generate the CSS output automatically.
Failing to account for the guaranteed-invalid value cascade. When a custom property resolves to invalid, the browser uses the inherited value for inherited properties and the initial value for non-inherited properties — not the unset value or the value from a previous rule. For example, if background is set to var(--bg-color) and --bg-color is invalid, background does not fall back to the previous background rule; it uses the initial value (transparent). This is why providing fallbacks in var() is important for any property where the invalid cascade behavior would be harmful.
Best practices for reliable custom properties
Use a consistent naming scheme and document it. All-lowercase kebab-case is the dominant convention and avoids case-sensitivity bugs. Prefix component-specific properties with the component name: --nav-bg, --nav-text, --nav-border-radius. This makes it immediately clear which component owns the property and prevents accidental overrides from unrelated components that happen to use the same token name.
Always provide fallback values in var() for any property used outside a tightly controlled component boundary. var(--primary-color, #6366f1) ensures the element displays correctly even if the token file fails to load or the property is accidentally removed from the token set. The fallback value also serves as documentation of the expected type and scale.
Register critical custom properties with @property for browser-enforced type safety and smooth animation support. Properties that animate (such as --hue, --saturation, or --progress) must be registered with the correct syntax type for the browser to interpolate them. Properties that are used in calc() benefit from registration with a length or number type because the browser can catch type mismatches at assignment time rather than failing silently at computed value time.
Keep a token reference in your repository. A markdown file or a Storybook page that lists all available custom properties, their types, default values, and which components use them reduces the risk of typos and orphaned properties. Tools like Style Dictionary, Token Studio, and Theo can generate this documentation automatically from a JSON token source, keeping it in sync with the actual CSS output.
For teams that need both CSS and JavaScript access to design tokens, maintain a single tokens file — a _tokens.css partial or a tokens.js module — that exports both CSS custom properties and JavaScript constants from one source of truth. Build tooling like Style Dictionary can generate both formats simultaneously from a shared JSON definition, ensuring that the --color-primary custom property and the COLOR_PRIMARY JavaScript constant always match without manual synchronization.
Custom property not working — fix checklist
- ✓Confirm the property is declared on :root or an ancestor (not a sibling or unrelated element)
- ✓Check for case typos: --myColor and --mycolor are different properties
- ✓Search the Computed tab for the exact property name to find its resolved value
- ✓Add var() fallback values for any property used outside a controlled component: var(--color, #333)
- ✓Check for circular references by replacing one property temporarily with a literal value
- ✓Remove any var() from media query conditions and use preprocessor variables instead
- ✓Verify JavaScript setProperty uses double dashes and no leading spaces in the name
- ✓Test on Safari 15+ specifically for @property registration and calc() edge cases
Related guides
Frequently asked questions
Why does my CSS custom property have no effect even though it is declared on :root?
Check for a typo in the property name — custom property names are case-sensitive, so --myColor and --mycolor are different properties. Also confirm the consuming rule is using var(--my-property) with exactly the same spelling. Open DevTools Computed tab, search for the property name, and compare the declaration and usage characters side by side.
Can I use a CSS custom property inside a media query condition?
No. var() inside a media query condition like @media (max-width: var(--bp)) does not work. Custom properties are resolved per-element in the computed value cascade, and media queries are evaluated before any element exists. Use a Sass variable or PostCSS env() variable that resolves to a literal value at build time, or use Container Queries for element-level responsive behavior.
What is the guaranteed-invalid value and how does it affect layout?
When var() references an undefined custom property, it returns the guaranteed-invalid value. This invalidates the containing declaration at computed value time. For inherited properties, the element uses the inherited value from its parent. For non-inherited properties, it uses the initial value (for example, transparent for background, auto for width). The previous rule's value is not used as a fallback, which is why cascading fallback rules do not work as expected.
How do I set a CSS custom property from JavaScript?
Use element.style.setProperty('--color', '#f00') for element-scoped inline assignment, or document.documentElement.style.setProperty('--color', '#f00') to set it globally on the html element. Read the resolved value with getComputedStyle(element).getPropertyValue('--color').trim(). The trim() removes leading whitespace that browsers sometimes include in the returned string.
Do CSS custom properties work inside Shadow DOM?
Custom properties declared on the host element are inherited into the Shadow DOM. Properties declared inside the Shadow DOM are not visible outside it. This is the standard mechanism for theming web components: the component exposes --component-bg and --component-color as its theming API, and the host document sets those values on the component element.
What causes a circular reference error with custom properties?
If --a: var(--b) and --b: var(--a), both properties form a cycle and resolve to the guaranteed-invalid value at computed time. The browser detects cycles by tracing the dependency graph during computed value resolution. Break the cycle by introducing a property with a literal value that both --a and --b can reference without depending on each other.
Can I animate CSS custom properties?
Only if the property is registered with @property and a typed syntax descriptor. Unregistered custom properties are treated as opaque strings and cannot be interpolated between values. Registered properties with syntax like angle, length, color, or number can be animated and transitioned by the browser because it knows how to interpolate the type.
Why does my custom property work in Chrome but not in Safari?
Safari 14 and earlier had incomplete support for @property registration and some edge cases with custom properties inside calc(). A common workaround is to store literal values in custom properties and apply calc() at the usage site rather than storing a calc() result as the custom property value. Check your target Safari version against caniuse.com for @property and custom property support tables.
How do I remove a custom property set by JavaScript?
Use element.style.removeProperty('--my-color'). Setting it to an empty string with setProperty does not remove it — it sets the value to an empty whitespace string, which is valid and will override inherited values. removeProperty restores normal cascade inheritance so the element can receive the value from its parent or :root.
All tools run in your browser. Your data never leaves your device. Last updated: 2026-05-06.