Theming
Theme every c2n component from a handful of design tokens with @c2n/theme, or reach for any component variable directly.
Every c2-* component is styled exclusively through its own CSS custom properties, named --c2-<component>__<part>[__<state>]--<property> (about 700 in total, listed on each component’s API tab). They stay fully independent: you can theme an app by setting those variables one by one, without any other layer.
@c2n/theme adds a convenience layer on top: a small set of design tokens and a generated base theme that maps every component variable onto them. Set ~35 tokens and every component follows; any component variable you set yourself still wins.
Both stylesheets put their values in the c2n-theme cascade layer, and a rule outside any layer beats a layered one whatever its specificity or source order. So your overrides never need a stronger selector than :root.
Installation
npm install @c2n/components@c2n/components brings in @c2n/theme and re-exports its stylesheets as @c2n/components/theme.css, base.css and tokens.css.
import '@c2n/components/theme.css' // tokens + base theme, once at the app rootThen override the tokens that differ in your brand:
:root {
--c2-theme--color-primary: #7c3aed;
--c2-theme--color-primary-hover: #6d28d9;
--c2-theme--radius-md: 10px;
--c2-theme--font-family: 'Inter', system-ui, sans-serif;
}How the layers stack
- Tokens —
--c2-theme--<name>. Shared colours, type, shape, focus ring, motion and elevation. Defined bytokens.css, overridden by your app. - Base theme —
base.csssets each component variable tovar(--c2-theme--<token>, <component default>)on:root. It is generated from the components’ manifests, so coverage grows with the library. - Component variables — the fine-grained escape hatch.
--c2-button__container--background-color: crimsonon an element, a class or:rootbeats the base theme, which sits in thec2n-themelayer. - Application variant components — repeated looks wrapped in your own components (a class, a wrapper, or a Lit subclass) built from the two layers above.
/* What base.css emits for one variable: the token first, the component's own default as the innermost fallback. */
--c2-list--border-top: var(--c2-theme--border, var(--c2-theme--border-width, 1px) solid var(--c2-theme--color-outline, #e4e4e7));Composite tokens such as --c2-theme--border and --c2-theme--focus-ring wrap their primitives, so you can set the whole shorthand (--c2-theme--border: 2px dashed hotpink) or just the colour inside it (--c2-theme--color-outline).
Dark mode
tokens.css defines light values on :root and dark values under [data-theme='dark'] and the .c2-dark class, with the OS preference as fallback when no data-theme attribute is present. data-theme="light" / .c2-light opts a subtree out.
A token you override on :root holds in both modes, the OS fallback included. When your brand needs a different dark value, set the token again under your dark selector:
:root {
--c2-theme--color-primary: #7c3aed;
}
[data-theme='dark'] {
--c2-theme--color-primary: #a78bfa;
}
@media (prefers-color-scheme: dark) {
:root:not([data-theme='light']) {
--c2-theme--color-primary: #a78bfa;
}
}If your app puts its own CSS in cascade layers (Tailwind v4 does), the layer declared first is the weakest, so declare c2n-theme before any import: @layer c2n-theme, theme, base, components, utilities;. Otherwise a framework imported before the theme leaves c2n-theme on top of its layers.
<html data-theme="dark">
…
<section class="c2-light">Light island inside a dark page</section>
</html>Because the selectors are not tied to the root element, any subtree can be inverted: a preview canvas, a chat panel, a sidebar.
Bring your own tokens
If your app already has a token system, skip tokens.css, load only base.css, and bridge your tokens onto the --c2-theme--* names. Dark mode then follows your own switch automatically.
@import '@c2n/components/base.css';
:root {
--c2-theme--color-primary: var(--brand-600);
--c2-theme--color-on-primary: var(--brand-on-600);
--c2-theme--color-surface: var(--surface);
--c2-theme--color-on-surface: var(--text);
--c2-theme--color-outline: var(--border);
--c2-theme--radius-md: var(--radius);
--c2-theme--font-family: var(--font-sans);
--c2-theme--focus-ring: 2px solid var(--brand-600);
}Tokens
| Token | Light | Dark | Description |
|---|---|---|---|
--c2-theme--color-primary | #0265dc | #5aa3ff | Accent colour: filled buttons, selected states, focused borders, links. |
--c2-theme--color-primary-hover | #0154b8 | #7bb6ff | Accent colour on hover. |
--c2-theme--color-primary-active | #01469a | #9cc9ff | Accent colour while pressed. |
--c2-theme--color-on-primary | #ffffff | #032a5c | Text and icons drawn on the accent colour. |
--c2-theme--color-primary-container | #edf1fe | #0f2d5c | Soft accent surface, e.g. a selected list item. |
--c2-theme--color-primary-glow | rgba(2, 101, 220, 0.2) | rgba(90, 163, 255, 0.28) | Accent tint for luminous borders and decorative glows. |
--c2-theme--color-surface | #ffffff | #18181b | Default surface of inputs, lists, cards, dialogs. |
--c2-theme--color-surface-container-low | #fafafa | #1f1f23 | Slightly raised surface: side navigation, read-only fields, subtle hover. |
--c2-theme--color-surface-container | #f4f4f5 | #27272a | Hover surface for rows and icon buttons. |
--c2-theme--color-on-surface | #18181b | #f4f4f5 | Primary text colour. |
--c2-theme--color-on-surface-variant | #71717a | #a1a1aa | Secondary text, placeholders, icons, supporting text. |
--c2-theme--color-outline | #d4d4d8 | #3f3f46 | Resting border colour of inputs, cards and lists. |
--c2-theme--color-outline-variant | #e4e4e7 | #27272a | Hairline dividers and light borders. |
--c2-theme--color-outline-strong | #a1a1aa | #52525b | Border colour on hover. |
--c2-theme--color-error | #dc2626 | #f87171 | Error borders, error text, required indicators. |
--c2-theme--color-scrim | rgba(9, 9, 11, 0.45) | rgba(0, 0, 0, 0.6) | Backdrop behind dialogs and drawers. |
--c2-theme--color-inverse-surface | #18181b | #f4f4f5 | High-contrast surface, e.g. tooltips. |
--c2-theme--color-on-inverse-surface | #fafafa | #18181b | Text drawn on the inverse surface. |
--c2-theme--color-dark-surface | #18181b | same | Surface of a block that is dark in both themes: terminals, dark sidebars, dark bubbles. |
--c2-theme--color-dark-surface-container | #27272a | same | Raised surface inside a dark block: a hovered or selected row, a header strip. |
--c2-theme--color-on-dark-surface | #fafafa | same | Text and icons on the dark surface. |
--c2-theme--color-on-dark-surface-variant | #a1a1aa | same | Secondary text on the dark surface: metadata, timestamps, muted labels. |
--c2-theme--color-dark-outline | #3f3f46 | same | Borders and dividers inside a dark block. |
--c2-theme--color-on-fill | #ffffff | same | Text and icons on a saturated colour fill other than the primary: status buttons, coloured events, brand badges. |
--c2-theme--font-family | unset | same | Font family of every component. Unset by default so components inherit the page font. |
--c2-theme--font-size-sm | 12px | same | Small text: supporting text, tooltips, descriptions, timestamps. |
--c2-theme--font-size-md | 14px | same | Body text: buttons, inputs, list items, dialog content. |
--c2-theme--font-weight-medium | 500 | same | Medium weight: buttons, headers, tooltips. |
--c2-theme--font-weight-semibold | 600 | same | Semibold weight: dialog titles, list headings, avatars. |
--c2-theme--radius-sm | 4px | same | Small radius: checkboxes, link buttons, inline controls. |
--c2-theme--radius-md | 6px | same | Default radius: buttons, inputs, list items, tooltips. |
--c2-theme--radius-lg | 8px | same | Large radius: cards, panels, popovers. |
--c2-theme--radius-xl | 14px | same | Extra large radius: dialogs. |
--c2-theme--radius-full | 999px | same | Pill / circle radius: avatars, icon buttons. |
--c2-theme--border-width | 1px | same | Width of every themed border. |
--c2-theme--border | unset | same | Complete resting border shorthand. Unset by default: it falls back to `border-width solid color-outline`. |
--c2-theme--focus-ring | 2px solid rgba(2, 101, 220, 0.4) | 2px solid rgba(90, 163, 255, 0.5) | Focus-visible outline of every component. |
--c2-theme--disabled-opacity | 0.38 | same | Opacity of disabled components. |
--c2-theme--motion-scale | 1 | same | Multiplier applied to every transition and animation duration (0 disables motion). |
--c2-theme--shadow-sm | 0 1px 3px rgba(0, 0, 0, 0.18) | 0 1px 3px rgba(0, 0, 0, 0.5) | Shadow of small raised controls, such as an avatar's remove button. |
--c2-theme--shadow-md | 0 8px 24px rgba(24, 24, 27, 0.08) | 0 8px 24px rgba(0, 0, 0, 0.45) | Shadow of popovers, menus and tooltips. |
--c2-theme--shadow-lg | 0 24px 60px rgba(0, 0, 0, 0.25) | 0 24px 60px rgba(0, 0, 0, 0.6) | Shadow of dialogs and drawers. |
--c2-theme--chart-series-1 | #0265dc | #5aa3ff | First categorical series. Matches the accent, so a single-series chart reads as brand. |
--c2-theme--chart-series-2 | #ea580c | #fb923c | Second categorical series: orange, the Okabe-Ito counterpart to the blue. |
--c2-theme--chart-series-3 | #0f766e | #2dd4bf | Third categorical series: teal. |
--c2-theme--chart-series-4 | #db2777 | #f472b6 | Fourth categorical series: pink. |
--c2-theme--chart-series-5 | #a16207 | #fbbf24 | Fifth categorical series: gold, dark enough not to collapse into the orange. |
--c2-theme--chart-series-6 | #7c3aed | #a78bfa | Sixth categorical series: violet. |
--c2-theme--chart-series-7 | #0891b2 | #22d3ee | Seventh categorical series: cyan. |
--c2-theme--chart-series-8 | #52525b | #a1a1aa | Eighth categorical series: neutral grey, for an "other" or residual bucket. |
--c2-theme--chart-positive | #16a34a | #4ade80 | Rising values: an up candle, a positive sparkline. Separate from the accent so direction and brand move independently. |
--c2-theme--chart-negative | #dc2626 | #f87171 | Falling values: a down candle, a negative sparkline. Separate from `color-error` so a brand can recolour direction without recolouring failure. |
What the base theme covers
Colours, backgrounds, borders, radii, focus rings, disabled opacity, font sizes and weights, durations and shadows with a concrete default are mapped. Sizes, paddings, gaps, identity colours (avatar fallbacks), status colours and the code viewer’s syntax theme stay component-specific by design, and variables without a default (they inherit) are left untouched. @c2n/theme/report.json lists, per package, what is mapped and what is not.