CSS Custom Properties for Theming a Multi-Brand Site
Switch brands with a data attribute, and avoid the derived-token trap that freezes hover colours on the wrong value.
One component set, several brands. The way to do it is to keep brand values in custom properties and switch them with an attribute. There is one trap in it, and it is easy to hit.
Step 1: brand tokens and a component that reads them
:root {
--brand: #034bff;
--brand-contrast: #ffffff;
--radius: 6px;
}
[data-brand="harbor"] { --brand: #0b6e4f; --radius: 2px; }
[data-brand="ember"] { --brand: #b42318; --radius: 999px; }
.btn {
background: var(--brand);
color: var(--brand-contrast);
border-radius: var(--radius);
border: 0;
padding: 8px 16px;
}
<div data-brand="harbor"><button class="btn">Buy</button></div>
<div data-brand="ember"><button class="btn">Buy</button></div>
Chrome 154 computed the background and corner radius of the three buttons (default, harbor, ember):
default rgb(3, 75, 255) 6px
harbor rgb(11, 110, 79) 2px
ember rgb(180, 35, 24) 999px
No component rule changed. Only the attribute did.
Step 2: add a derived hover colour, and see the trap
:root {
--brand: #034bff;
--brand-hover: color-mix(in srgb, var(--brand) 85%, black);
}
.btn:hover { background: var(--brand-hover); }
This looks right and fails for the other brands. I hovered the default button and the harbor button:
hover default color(srgb 0.01 0.25 0.85)
hover harbor color(srgb 0.01 0.25 0.85)
Both are the same dark blue. The cause is that var(--brand) inside --brand-hover is resolved on :root, where --brand is still the default blue, and the finished value is then inherited. The harbor block changes --brand, but --brand-hover has already been computed.
Step 3: declare derived tokens where the brand changes
:root, [data-brand] {
--brand-hover: color-mix(in srgb, var(--brand) 85%, black);
}
Now each brand element recomputes the hover colour from its own --brand. After the change:
hover default color(srgb 0.01 0.25 0.85)
hover harbor color(srgb 0.0366667 0.366667 0.263333)
Harbor's hover is now a darker green. The default is unchanged.
⚠ Warning: Any token built from another token has to be declared at, or below, the place that changes the source. Otherwise it freezes with the source's value from the parent.
Check each brand's contrast
White text on #0b6e4f is 6.25:1 and on #b42318 it is 6.57:1. Both pass 4.5:1. Check contrast every time a brand is added, because a pale brand colour with white text can fail.
Common mistakes
- Declaring derived tokens only on
:root. - Putting brand-specific rules in components (
.btn--harbor) instead of tokens. - Letting a brand override a primitive token that other brands share.
Related course: HTML and CSS Advanced.
About
Switch brands with a data attribute, and avoid the derived-token trap that freezes hover colours on the wrong value.
- Responsible
- John G
- Last Update
- 08/20/2026
- Reading Time
- 3 min
Reviews
Sign in to write a reviewComments (0)
Sign in to commentNo comments yet.