@c2n/theme
Design tokens for the @c2n/* web components, plus a generated base theme that maps every component CSS variable onto them.
Components are themed through their own CSS custom properties (--c2-button__container--background-color, …, about 700 in total). @c2n/theme adds a convenience layer on top: ~35 shared tokens named --c2-theme--<name>. The base theme sets each component variable to var(--c2-theme--<token>, <component default>) at :root, so an application only has to define the tokens. Any component variable you set yourself still wins.
npm install @c2n/theme
import '@c2n/theme/theme.css' // tokens + base theme
/* Your app: override a few tokens, every component follows. */
:root {
--c2-theme--color-primary: #7c3aed;
--c2-theme--radius-md: 10px;
--c2-theme--font-family: 'Inter', system-ui, sans-serif;
}
Files
| Export | Content |
|---|---|
@c2n/theme/theme.css | tokens.css + base.css |
@c2n/theme/tokens.css | Token defaults: light on :root, dark under [data-theme='dark'] / .c2-dark and as the prefers-color-scheme: dark fallback |
@c2n/theme/base.css | --c2-<component>…: var(--c2-theme--…, <default>) for every mapped component variable. Use it alone if your app already defines the tokens |
@c2n/theme/tokens.js | The token table as TypeScript (tokens, TokenDef) |
@c2n/theme/tokens.json | Tokens + the component variable → token mapping |
@c2n/theme/report.json | Coverage per package and the list of variables left unmapped |
Dark mode
Set data-theme="dark" (or the c2-dark class) on <html> or on any subtree; without it the OS preference applies. data-theme="light" / c2-light opts a subtree out.
Cascade layer
tokens.css and base.css put every rule in the c2n-theme cascade layer. An unlayered rule beats a layered one whatever its specificity or source order, so a plain :root { --c2-theme--color-primary: … } overrides the token in light mode, under data-theme="dark" and under the OS dark fallback alike, and so does a component variable you set yourself. If your app puts its own styles in layers, declare c2n-theme first, in a @layer statement before any import, so it stays the lowest: @layer c2n-theme, base, components;. A layered framework imported before the theme otherwise leaves c2n-theme on top of its layers.
Layers
- Tokens —
--c2-theme--*, set by the app. - Base theme — generated mapping, ships here.
- Component variables —
--c2-<component>__<part>[__<state>]--<property>, the fine-grained escape hatch. - App variant components — repeated looks wrapped in your own components (see the docs site guide).
Development
npm run build -w packages/tools/theme compiles src/tokens.ts and runs scripts/theme-generator, which reads every @c2n/*/custom-elements.json, classifies each documented CSS variable (scripts/theme-generator/classify.ts, exceptions in overrides.ts) and writes dist/. The build depends on every component build so the manifests are fresh; the console prints a coverage table and dist/report.json lists what stayed unmapped and why.
A component may document a shorthand variable behind its per-side ones (--c2-details--border behind --c2-details--border-top and the other three, compiled as var(--c2-details--border-top, var(--c2-details--border, …))). The generator reads those fallbacks from the package's built dist/ and, when a per-side variable maps to exactly the value its shorthand maps to, writes only the shorthand to base.css (scripts/theme-generator/shorthands.ts): writing the sides would make the shorthand unreachable under the theme. A per-side variable you set still wins; dist/report.json lists the omitted sides under coveredByShorthand. npm test -w packages/tools/theme runs the generator's unit tests.
npm run dev -w packages/tools/theme opens a Vite harness (index.html) with themed components, a dark-mode toggle and token overrides.