Overriding a CSS variable on a child does not rebuild a composite custom property inherited from its parent. The parent has already resolved the nested reference. To use the child’s value, declare the composite on that child or assemble the final property there.

This guide explains that behavior and provides a reproducible desktop exercise in Chrome on a Mac. The task is to make a component’s border follow its local color override while keeping shared defaults.

Why does the inherited composite keep the parent’s color?

Consider this CSS fragment. It assumes a .sample-tile element inside a .sample-board element:

.sample-board {
  --sample-color: teal;
  --sample-border: 6px solid var(--sample-color);
}

.sample-tile {
  --sample-color: darkorange;
  border: var(--sample-border);
}

The child’s border remains teal. The CSS Variables specification states that custom properties resolve their nested var() functions at computed-value time, before inheritance. Consequently, the child inherits a composite equivalent to 6px solid teal; it does not inherit an expression that will substitute the child’s --sample-color. CSS Custom Properties for Cascading Variables Module Level 1.

There are two separate properties here. Setting --sample-color on the child changes that property’s local value. It does not also declare a new value for --sample-border. MDN’s custom-property reference describes ordinary, unregistered custom properties as inherited, with computed values that have their variables substituted. Custom properties: CSS variables.

The sequence in this example is:

  1. The parent computes --sample-color as teal.
  2. Its --sample-border substitutes that color, producing 6px solid teal.
  3. The child inherits that composite and separately sets its own color token to darkorange.
  4. The child’s border declaration consumes the inherited teal composite.

This is not a permanent snapshot of the parent. If the parent’s color changes, its composite can update and be inherited again. The boundary is the element where the nested reference resolves: a descendant override does not reach upward into that computation. CSS Variables: resolving dependencies.

Reproduce the behavior in Chrome on a Mac

Use a blank page so existing page styles do not interfere. Open a new Chrome tab, enter about:blank in the address bar, then press Command + Option + J to open the Console. Google documents this Mac shortcut in its DevTools guide. Open Chrome DevTools.

The following complete browser-console program creates three labeled abstract shapes and their stylesheet. It needs no libraries, images, network requests, or existing page elements. Run the entire block once on the blank page. Its expected results below are derived from the documented CSS rules; they are not presented as recorded browser measurements.

(() => {
  const board = document.createElement("section");
  board.className = "sample-board";

  const style = document.createElement("style");
  style.textContent = `
    .sample-board {
      --sample-color: teal;
      --sample-border: 6px solid var(--sample-color);
      display: flex;
      flex-wrap: wrap;
      gap: 24px;
      padding: 32px;
      font: 16px system-ui;
    }

    .sample-board .sample-tile {
      --sample-color: darkorange;
      display: grid;
      place-items: center;
      width: 120px;
      height: 80px;
      border: var(--sample-border);
      border-radius: 24px;
      background: white;
      color: black;
    }

    .sample-board .sample-local {
      --sample-border: 6px solid var(--sample-color);
    }

    .sample-board .sample-direct {
      border: 6px solid var(--sample-color);
    }
  `;

  const samples = [
    ["Inherited", "sample-inherited"],
    ["Local", "sample-local"],
    ["Direct", "sample-direct"]
  ];

  const tiles = samples.map(([label, className]) => {
    const tile = document.createElement("div");
    tile.className = `sample-tile ${className}`;
    tile.textContent = label;
    board.append(tile);
    return tile;
  });

  document.head.append(style);
  document.body.append(board);

  function report(stage) {
    console.log(stage);
    console.table(tiles.map((tile) => {
      const computed = getComputedStyle(tile);
      return {
        sample: tile.textContent,
        localColor: computed
          .getPropertyValue("--sample-color").trim(),
        composite: computed
          .getPropertyValue("--sample-border").trim(),
        paintedBorder: computed.borderTopColor
      };
    }));
  }

  report("Initial values");
  board.style.setProperty("--sample-color", "rebeccapurple");
  report("After changing the parent color");
})();

The program prints two tables. The expected border colors are:

Sample Initially After the parent changes Reason
Inherited Teal Purple Uses the parent’s computed composite
Local Orange Orange Declares its composite on the child
Direct Orange Orange Substitutes the local color directly in border

All three children retain darkorange as their local color token. The useful diagnostic is the Direct sample: its inherited composite still contains the parent’s color, but its painted border uses the local token because that border declaration does not consume the composite.

getComputedStyle() reads resolved property values after active stylesheets have been applied. It can therefore reveal what an element actually receives, rather than merely showing the authored expression. Color serialization may appear as an rgb() value instead of the original color name. Window: getComputedStyle() method.

Reload the blank tab to clear the exercise. For an authored page, put the chosen CSS fix in its stylesheet; console changes are useful for diagnosis but do not edit your source files.

Which fix should you choose?

Declare the composite at the component boundary

The Local sample keeps the reusable border token and declares its recipe on the same element that supplies the override. Here is the relevant CSS fragment from the program:

.sample-board .sample-local {
  --sample-color: darkorange;
  --sample-border: 6px solid var(--sample-color);
  border: var(--sample-border);
}

Choose this pattern when several declarations need the same assembled value. Put primitive defaults on an ancestor and the composite declaration on the component that owns the customization. Custom properties can be declared on specific elements and inherit through their descendants; they do not need to live on :root. Using CSS custom properties.

The placement remains significant farther down the tree. If a grandchild overrides the primitive again, an inherited composite from this component will still reflect this component’s computation. Declare the recipe again at any descendant boundary that must assemble a different result.

Assemble the final property where it is used

The Direct sample removes the intermediate token from the border declaration. This CSS fragment is its essential rule:

.sample-board .sample-direct {
  --sample-color: darkorange;
  border: 6px solid var(--sample-color);
}

Choose this when only one declaration needs the combination. You can still share separate width, style, and color tokens, then compose them in the component’s border declaration. MDN documents that var() can substitute all or part of another property’s value. Using CSS custom properties.

Override the complete composite when it is the public control

Sometimes the component’s intended customization is the complete border. In that case, provide a local replacement for --sample-border, such as 6px solid darkorange, instead of changing only its former input.

Use this decision aid when choosing the component contract:

Intended customization Suitable pattern
One local color controls one border Compose directly in border
Several declarations share one assembled value Declare the composite on the component
A caller supplies the entire border Override the composite itself
Descendants must use exactly the parent’s assembled value Keep the inherited composite

Why don’t specificity, fallback, or inheritance keywords rebuild it?

First confirm whether the primitive override actually won. If the child’s resolved --sample-color is already darkorange, making that same declaration more specific will not change the separate inherited --sample-border. Both properties already have values; the problem is where the composite was assembled.

Similarly, --sample-border: inherit takes the parent’s computed value. Ordinary inheritance transfers computed values rather than rerunning the parent’s authored declaration in the child’s context. Inheritance.

A fallback such as var(--sample-border, 6px solid darkorange) also leaves a valid inherited teal border untouched. A var() fallback handles a custom property without a usable value; it does not compare that value with a descendant’s preferred inputs. var() CSS function.

Keep invalid values separate from this inheritance issue. For example, an unregistered token containing 12px can exist as a custom-property value, yet produce an invalid result when substituted into border-color. The fallback inside that var() call is not a general type-checking alternative. MDN explains why custom-property substitution can create declarations that become invalid at computed-value time. Using CSS custom properties.

Does registering the variable with @property fix this?

@property lets you specify a custom property’s syntax, initial value, and inheritance behavior. It does not make a parent’s inherited composite reevaluate against a child’s overridden inputs. Registering the primitive with inherits: false would change how that primitive is obtained, while registering the composite that way would stop its normal inheritance. Neither supplies the missing local recipe. @property CSS at-rule.

Registration also has a different compatibility boundary. MDN marks ordinary custom properties as widely available across browsers since April 2017, while @property reached Baseline availability in July 2024 and may be unavailable in older browsers. The two demonstrated fixes require no registration. Custom properties: CSS variables, @property CSS at-rule.

How to diagnose the same problem in your stylesheet

Use the three-column comparison from the exercise on the element whose appearance is wrong:

  1. Read the primitive token, such as --sample-color.
  2. Read the composite token, such as --sample-border.
  3. Read the rendered property, such as border-top-color.
  4. Find the element where the composite declaration applies.
  5. Move or repeat that declaration at the intended customization boundary, or compose directly in the final property.

To locate the relevant rules in Chrome, right-click the shape and choose Inspect. DevTools selects the element in Elements, and the Styles tab displays its applied CSS rules. Open Chrome DevTools.

When the primitive shows the desired local value but the composite still contains an ancestor’s value, inspect the composite’s declaration site first. Define the recipe where its inputs should be read, then verify both the resolved token and the painted result. That gives the component an explicit, predictable customization boundary.

Sources