Scoped overrides
Change one area, one skin or one part without touching the rest. Tokens on an element, @scope rules and nested providers, and how the cml.* layers decide who wins.
// you'll learn
- Three ways to limit an override: a token on an element, an
@scoperule, a nested provider - The order of the
cml.*layers, and why your CSS wins without!important - The pitfalls: islands, states you override by accident,
!important, reduced motion
Try it
The card has a "danger zone" (<div className="danger-zone">). Each option restyles only that area; the code shown with it is what runs.
Workspace
Danger zone
Deleting the workspace removes 14 projects for everyone.
<div className="danger-zone">
<Text>Danger zone</Text>
<Button variant="outline">Transfer</Button>
<Button variant="danger">Delete workspace</Button>
</div>
No override — the skin as it ships.
Pick the narrowest tool
| Tool | Use it for | Reaches |
|---|---|---|
| A token on an element | Values: color, radius, size, spacing | That element and everything inside it |
@scope (…) to (…) | Anything a token can't do | The scope root down to the limit you set |
A nested StyleProvider | A different skin, mode or brand | Its subtree, popups included |
Start with tokens. A token keeps the skin's states working (hover, focus, disabled) because the skin still writes the rules; you only change the values they read.
/* Taller controls in the checkout only */
.checkout {
--cml-control-height: 2.75rem;
--cml-field-height: 2.75rem;
}The element must be inside the provider. Every [data-style] element declares all tokens again, so a class on a wrapper around the StyleProvider is overwritten before it reaches the components.
When the change is not a value (a border that isn't there, a different layout), write a rule inside @scope. Add to ([data-style]) so it stops at nested skins, the way the skin files do:
@scope (.pricing) to ([data-style]) {
[data-slot="card"] {
border-style: dashed;
}
}For a whole different look in one area, nest a provider. It is an island: it starts from clean tokens, and its popups keep its skin.
<StyleProvider styleName="minimal">
<Article />
<StyleProvider styleName="retro" mode="dark">
<Terminal />
</StyleProvider>
</StyleProvider>
The layer order
base.css declares four layers before anything else. Later layers win over earlier ones, and CSS that is not in any layer wins over all of them:
@layer cml.tokens, cml.components, cml.style, cml.preferences;| Layer | Holds |
|---|---|
cml.tokens | Every token's default (the minimal values) on :root and [data-style] |
cml.components | The component rules, mostly inside :where(), so they add no specificity |
cml.style | The skin files: token values on [data-style="name"] and the skin's @scope rules |
cml.preferences | Reduced motion and forced colors |
| your unlayered CSS | Wins over all of the above, whatever the specificity |
If your own CSS lives in a layer (a design system, a CSS framework), order the layers once, at the top of your entry stylesheet, before any import. A layer listed after cml wins over all of its sub-layers:
@layer cml, app;
@import "@camaleo/styles/base.css";
@import "@camaleo/styles/glass.css";
@layer app {
[data-style] { --cml-radius-surface: 20px; }
}Using Tailwind? Its utilities live in layers too, and which side wins depends on the same order. The Tailwind CSS guide has the line to add.
Specificity pitfalls
- Unlayered rules beat skin states.
[data-slot="button"] { background: navy }outside any layer also beats every skin's:hover,[data-disabled]and:focus-visiblerules, so the button stops reacting. Change--cml-accentand--cml-accent-hoverinstead, or write the states yourself. !importantturns the layers upside down. An important declaration in an earlier layer beats one in a later layer, and any layered important beats an unlayered one. You never need it to beat the library; leave it out.- Your CSS also beats
cml.preferences. If you add a transition or an animation, put it inside@media (prefers-reduced-motion: no-preference)yourself. - A scope without a limit leaks into islands.
@scope (.pricing)also styles a nested provider inside.pricing; addto ([data-style]). - A token set too high is lost. On
:rootor outside the provider, it is overwritten by the provider's own declarations. Use[data-style]or an element inside.
Before you ship it
- Tokens first; a rule only when no token covers it
- Token overrides on an element inside the provider
@scoperules end withto ([data-style])- No
!important; layered CSS listed aftercmlin one@layerstatement - Hover, focus and disabled still visible after your change, in light and dark