Overriding
elements man style/overriding Read as markdownThe system is designed to be overridden cleanly. The cascade layer order
guarantees your app's CSS wins, and semantic tokens let you re-skin everything
from one place. You never need !important, and you never need to out-specify a
component selector.
The cascade contract
The root entry declares one layer order:
@layer reset, tokens, base, components, utilities, overrides;
Later layers beat earlier layers regardless of selector specificity. That single fact is the override contract:
- A low-specificity
bodyrule of yours in a later position beats a high-specificity component selector, because layer order outranks specificity. - The
overrideslayer is reserved for your app and sits last, so anything you put there wins over everything the package ships. - Unlayered CSS beats all layers. Any rule you write outside a layer automatically wins over the entire package. So even app CSS that isn't in a layer at all takes precedence.
This is why bare-element styling and components never need !important to be
overridden. You're never fighting specificity, only layer position, and yours is
higher.
Three altitudes of override
Pick the lowest altitude that does the job.
1. Retarget a token (system-wide). Change a value once and every component that reads it follows. This is the primary mechanism, so reach for it first.
:root {
--accent: #4a7bc4; /* rebrand every button, link, ring, selection */
--radius: 8px; /* rounder controls and surfaces everywhere */
--ink: #0f172a; /* shift the primary text color */
}
In a scaffolded app, app/shared/styles/vars.css is exactly this file. It's
imported by both the page and email baselines, so a token you set there applies
across the whole app.
2. Restyle a component family (targeted). Write a rule against the component
class in the overrides layer (or unlayered). Because you're in a later layer,
your rule wins:
@layer overrides {
.card {
box-shadow: var(--shadow-md);
border-radius: var(--radius-lg);
}
}
3. One-off tweaks (local). Use a utility class, an is- modifier, or a
page-scoped rule for a single element. Utilities already sit above components,
so they win; a page's own style.css rule wins as app CSS.
<div class="card shadow-lg rounded-xl">…</div>
Always use semantic tokens, not hex
When you override, reach for the semantic token, not a raw value or ramp step:
/* Do this: stays correct in light and dark mode */
.note { color: var(--ink-soft); background: var(--bg-soft); }
/* Not this: a hardcoded value opts out of theming and breaks dark mode */
.note { color: #475569; background: #f8fafc; }
A hardcoded color won't flip in dark mode and won't follow a rebrand. The
semantic tokens (--ink*, --bg*, --rule*, --accent*, the status triples)
exist so your overrides inherit theming for free.
Where app CSS goes
-
App-wide tokens →
app/shared/styles/vars.css(imported by the page and email baselines). -
App-wide rules →
app/shared/styles/page.css, after@import "@elements/style". -
A full brand theme → copy
themes/customer-example.cssintoapp/shared/styles/and import it frompage.cssafter the package. See branding. -
Page-specific rules → the page's own
style.css, which imports#app/shared/styles/page.cssand adds.<page-name>-*rules built on tokens.
Because all of these load after the package and either sit in overrides or run
unlayered, they win cleanly, without !important.