Theming
Three colours are set by hand. Surfaces, borders and secondary text are mixed from them with color-mix, so changing the background re-skins everything that sits on it.
The three colours
:root {
--ck-bg: oklch(0.985 0 0);
--ck-fg: oklch(0.205 0 0);
--ck-accent: oklch(0.52 0.18 316);
}That is a complete theme. Put the same three inside .dark for the dark scheme and you
are done.
Tokens you set
Defaults below are the light scheme, read from the stylesheet that ships with the package.
| Token | Default | What it does |
|---|---|---|
--ck-bg | oklch(0.985 0 0) | The page colour before the accent is mixed in. |
--ck-fg | oklch(0.205 0 0) | Body text. Everything secondary is mixed from it. |
--ck-accent | oklch(0.52 0.18 316) | Buttons, focus rings, links, selected rows. |
--ck-accent-hover | oklch(0.47 0.18 316) | The accent on hover. Not derived: contrast is a judgement call. |
--ck-accent-fg | oklch(0.99 0 0) | Text on top of the accent. |
--ck-tint | 1.5% | How much accent reaches the neutrals. 0% gives a grey page. |
--ck-tint-base | var(--ck-accent) | What gets mixed in. Dark mode uses a deepened accent so the page keeps its weight. |
--ck-radius-scale | 1 | Multiplies Tailwind's radius scale. 0 squares everything off. |
--ck-elevate | 72% | How far a raised surface lifts off the canvas. |
--ck-success | oklch(0.55 0.14 150) | Status colours. Plain values, not derived. |
--ck-warning | oklch(0.58 0.13 68) | |
--ck-danger | oklch(0.55 0.21 25) | |
--ck-info | oklch(0.55 0.15 250) | |
--ck-font-sans | ui-sans-serif, system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif | Body and UI typeface. |
--ck-font-mono | ui-monospace, 'SF Mono', SFMono-Regular, Menlo, Consolas, monospace | Used by Code, Kbd and Terminal. |
The contrast ratios are tokens too: muted, border, border-strong, fg-muted, fg-subtle, accent-soft control how far each derived colour sits from the canvas. Raise them for more contrast, lower them for a flatter look.
Tokens you get
These are mixed from the three above and recomputed wherever a theme is declared. Override one and the formula steps aside for that value.
| Token | Mixed from | Used for |
|---|---|---|
--ck-canvas | color-mix(in oklab, var(--ck-tint-base) var(--ck-tint), var(--ck-bg)) | The real page colour: --ck-bg with the tint mixed in. |
--ck-surface | color-mix(in oklab, white var(--ck-elevate), var(--ck-canvas)) | Cards, inputs, menus — anything sitting on the canvas. |
--ck-muted | color-mix(in oklab, var(--ck-fg) var(--ck-mix-muted), var(--ck-canvas)) | Quiet fills: hover states, table stripes, skeletons. |
--ck-border | color-mix(in oklab, var(--ck-fg) var(--ck-mix-border), var(--ck-canvas)) | Hairlines. |
--ck-border-strong | color-mix( in oklab, var(--ck-fg) var(--ck-mix-border-strong), var(--ck-canvas) ) | Borders that need to be seen, like a checkbox outline. |
--ck-fg-muted | color-mix(in oklab, var(--ck-fg) var(--ck-mix-fg-muted), var(--ck-canvas)) | Secondary text. |
--ck-fg-subtle | color-mix(in oklab, var(--ck-fg) var(--ck-mix-fg-subtle), var(--ck-canvas)) | Placeholders and captions. |
--ck-accent-soft | color-mix( in oklab, var(--ck-accent) var(--ck-mix-accent-soft), var(--ck-canvas) ) | The accent as a background tint. |
--ck-ring | var(--ck-accent) | Focus ring colour. Follows the accent unless you set it. |
Every one of them reaches Tailwind as a utility: bg-surface, text-fg-muted, border-border, bg-accent/12.
A custom accent
Set the hover shade and the text colour alongside it. Neither is derived, because readable contrast on a coloured button is a decision rather than a formula.
:root {
--ck-accent: #0d9488;
--ck-accent-hover: #0f766e;
--ck-accent-fg: #ffffff;
}Six presets ship with the kit — plum (the default), cobalt, pine, ember, gold and ink. Set data-accent on <html>, or assign theme.accent at runtime.
Typeface
:root {
--ck-font-sans: 'Hanken Grotesk Variable', ui-sans-serif, system-ui, sans-serif;
--ck-font-mono: 'JetBrains Mono', ui-monospace, monospace;
}Themes on part of a page
Put data-ck-theme on any element and that subtree gets its own theme. Set --ck-* variables on it and the derived tokens are recomputed there, which is how the theme generator shows light and dark side by side on one page.
<section data-ck-theme="dark" style="--ck-accent: oklch(0.75 0.15 160)">
<Card>Dark, green, and only in here.</Card>
</section>Overlays escape the subtree
Modal, Drawer, DropdownMenu, Popover and Tooltip render into <body>, so they follow the page theme rather
than the section they were opened from.Contrast
The defaults clear WCAG AA in both schemes: body text at 16.8:1 and 17.4:1, secondary text above
6:1, placeholder text above 4.5:1, and control outlines above 3:1. Two of the mix ratios — --ck-mix-border-strong and --ck-mix-fg-subtle — are set exactly where they
clear those thresholds, so lowering them for a softer look is a trade you are making knowingly.
A custom accent is the easy thing to get wrong, because --ck-accent-fg has to work on
top of it and the accent itself has to work as link text. The theme generator measures both, on the colours the browser actually paints, and
can keep rolling until everything passes.
At runtime
The theme object is plain runes state. Changes persist to local storage and the page
crossfades rather than snapping.
import { theme } from '@joshicodes/cock';
theme.mode = 'dark'; // 'light' | 'dark' | 'system'
theme.toggle();
theme.accent = 'pine'; // a preset, or any name you defined
theme.setToken('--ck-bg', '#f6f5f2'); // the scheme on screen
theme.setToken('--ck-bg', '#101014', 'dark'); // or a specific one
theme.setSharedToken('--ck-radius-scale', '0'); // both schemes at once
theme.clearTokens(); // back to the stylesheettheme.resolved is always 'light' or 'dark', even when the
mode is 'system'. theme.revision changes on every applied theme, which is
the thing to watch if you measure computed styles yourself.