Tokens
About 30 custom properties in @layer tokens — the only place visual decisions live. Replacing this layer is the entire theming story.
Overriding
Paste your own tokens after the keel link tag, inside the same @layer tokens — later declarations in the same layer win, so anything you leave out keeps its default.
<link rel="stylesheet" href="../keel.css">
<style>
@layer tokens {
:root {
--k-accent: hsl(160 84% 32%);
--k-radius: 4px;
--k-density: 0.9;
}
}
</style>
You don't have to write a full set by hand — the design system page generates one from a color, a mood, and a font pairing.
Color relationships
The accent shades derive from their seed via color-mix(): override --k-accent alone and strong, light, soft, and both transparencies re-derive automatically. The one exception is --k-on-accent, which stays explicit — contrast is a judgment the math can't make safely. The deeper story is on the concepts page.
@layer tokens {
:root { --k-accent: hsl(160 84% 32%); --k-on-accent: hsl(0 0% 100%); }
}
Auto dark
Opt-in: put data-k-auto-dark on the
html element and, when the visitor's OS prefers
dark, the neutral seeds flip to a night set — every derived
shade follows. It's a sensible default, not a designed theme;
sites that care about their dark mode should ship their own
tokens block instead (the design
page's Night and Dusk moods are a starting point). keel
also ships quiet print styles: chrome and overlays hidden,
bands flattened to ink on white.
<html lang="en" data-k-auto-dark>
Flat skin
Opt-in: put data-k-flat on the
html element to strip elevation — the shadow
scale collapses to none and the second surface
step is redrawn from the border, so cards, buttons and popovers
read as thin, borders-only shapes. It's a separate axis from
color, so it composes with any mood or with auto dark;
pair it with a smaller --k-radius and a denser
scale for the full range from designed to bare.
<html lang="en" data-k-flat>
Color
The swatches are live — each cell paints with its own token, so this reference re-renders in your palette.
| Token | What it does | Default |
|---|---|---|
--k-bg |
Page background. | hsl(228 20% 98.5%) |
--k-surface |
Raised surfaces: cards, alt bands, inputs, dialogs. | hsl(0 0% 100%) |
--k-surface-2 |
Second surface step: code blocks, table stripes, progress tracks. | hsl(228 18% 95%) |
--k-border |
Default border for cards, tables, rules, band edges. | hsl(228 14% 87%) |
--k-border-strong |
Stronger border where the edge must read: inputs, ghost buttons. | hsl(228 14% 76%) |
--k-text |
Body text color. | hsl(228 22% 14%) |
--k-text-soft |
Secondary text: leads, descriptions, table headers. | hsl(228 12% 36%) |
--k-text-faint |
Tertiary text: captions, hints, placeholders, footers. | hsl(228 9% 52%) |
--k-accent |
The brand color: links, buttons, focus rings, selection. | hsl(231 84% 56%) |
--k-accent-strong |
Darker accent for hover states and small text on light surfaces. Derives from the seed. | color-mix(in oklab, var(--k-accent), black 20%) |
--k-accent-light |
Lighter accent for accents on dark surfaces. Derives from the seed. | color-mix(in oklab, var(--k-accent), white 25%) |
--k-on-accent |
Text placed on the accent: button labels, accent bands. | hsl(0 0% 100%) |
--k-accent-soft |
Pale accent wash: tint bands, badges, marks. Derives from the seed, mixed into the page background. | color-mix(in oklab, var(--k-accent) 12%, var(--k-bg)) |
--k-accent-trans-10 |
Accent at 10% opacity. Genuinely transparent — the swatch shows the page behind it. | color-mix(in srgb, var(--k-accent) 10%, transparent) |
--k-accent-trans-20 |
Accent at 20% opacity: focus ring shadows. Transparent, like the 10% step. | color-mix(in srgb, var(--k-accent) 20%, transparent) |
--k-accent-2 |
The secondary brand color: secondary buttons and badges. Derived from the primary by harmony on the design page, or set exactly. | hsl(351 72% 44%) |
--k-accent-2-strong |
Darker secondary for hover states. Derives from the seed. | color-mix(in oklab, var(--k-accent-2), black 20%) |
--k-accent-2-light |
Lighter secondary for dark surfaces. Derives from the seed. | color-mix(in oklab, var(--k-accent-2), white 25%) |
--k-on-accent-2 |
Text placed on the secondary. | hsl(0 0% 100%) |
--k-accent-2-soft |
Pale secondary wash: secondary badges and .k-section--tint-2 bands. Derives from the seed, mixed into the page background. |
color-mix(in oklab, var(--k-accent-2) 12%, var(--k-bg)) |
--k-accent-2-trans-10 |
Secondary at 10% opacity. Genuinely transparent — the swatch shows the page behind it. | color-mix(in srgb, var(--k-accent-2) 10%, transparent) |
--k-accent-2-trans-20 |
Secondary at 20% opacity. Transparent, like the 10% step. | color-mix(in srgb, var(--k-accent-2) 20%, transparent) |
--k-accent-3 |
The tertiary/accent brand color: tertiary buttons and badges, highlights. Set exactly on the design page. Carries the same -strong, -light, -soft, and -trans-10/20 variants as the others. |
hsl(174 70% 28%) |
--k-on-accent-3 |
Text placed on the accent. | hsl(0 0% 100%) |
--k-accent-3-soft |
Pale accent wash: tertiary badges and .k-section--tint-3 bands. Derives from the seed, mixed into the page background. |
color-mix(in oklab, var(--k-accent-3) 12%, var(--k-bg)) |
--k-ok |
Status: success. Used by ok badges and alerts. | hsl(152 60% 30%) |
--k-warn |
Status: caution. Used by warn badges and alerts. | hsl(38 92% 34%) |
--k-danger |
Status: error. Used by danger badges and alerts. | hsl(4 74% 42%) |
Type
The scale is fluid — each step is a clamp() that breathes with the viewport, no breakpoints.
| Token | What it does | Default |
|---|---|---|
--k-font-display |
Headings and stat values. | system-ui, "Segoe UI", sans-serif |
--k-font-body |
Everything else. | system-ui, "Segoe UI", sans-serif |
--k-font-mono |
Code, kbd, kickers, eyebrows, the brand mark. | ui-monospace, Consolas, "Cascadia Mono", monospace |
--k-display-weight |
Weight of headings. Fonts with variable weight can go anywhere between 400 and 900. | 650 |
--k-tracking |
Letter-spacing of headings. | -0.015em |
--k-leading |
Body line-height. | 1.65 |
--k-leading-tight |
Heading line-height. | 1.12 |
--k-text-xs |
Smallest step: badges, hints, table headers, labels. | clamp(0.78rem, 0.75rem + 0.15vw, 0.84rem) |
--k-text-s |
Small step: code blocks, tables, small print, nav links. | clamp(0.88rem, 0.85rem + 0.18vw, 0.95rem) |
--k-text-m |
Body size. | clamp(1rem, 0.96rem + 0.22vw, 1.1rem) |
--k-text-l |
Large step: h4, leads, blockquotes. | clamp(1.15rem, 1.06rem + 0.45vw, 1.35rem) |
--k-text-xl |
h3. | clamp(1.4rem, 1.22rem + 0.9vw, 1.8rem) |
--k-text-2xl |
h2 and stat values. | clamp(1.8rem, 1.48rem + 1.6vw, 2.5rem) |
--k-text-3xl |
h1. | clamp(2.4rem, 1.9rem + 2.6vw, 3.6rem) |
Space
Every step multiplies by --k-density — one number loosens or tightens the whole page; steps 1–4 are static, 5–8 fluid.
| Token | What it does | Default |
|---|---|---|
--k-density |
Global spacing multiplier. 0.85 is compact, 1.15 is airy. | 1 |
--k-space-1 |
Hairline gaps: list items, hints, labels. | calc(0.25rem * var(--k-density)) |
--k-space-2 |
Tight padding: table cells, input blocks, badges. | calc(0.5rem * var(--k-density)) |
--k-space-3 |
Small gaps: clusters, heading margins, alerts. | calc(0.75rem * var(--k-density)) |
--k-space-4 |
The base rhythm: paragraph margins, stack gaps, card grids. | calc(1rem * var(--k-density)) |
--k-space-5 |
Card padding, split gaps, blockquote margins. Fluid. | calc(clamp(1.4rem, 1.2rem + 0.9vw, 2rem) * var(--k-density)) |
--k-space-6 |
Loose stacks, tight bands, horizontal rules, footers. Fluid. | calc(clamp(2.25rem, 1.85rem + 1.8vw, 3.4rem) * var(--k-density)) |
--k-space-7 |
Section band padding. Fluid. | calc(clamp(3.25rem, 2.5rem + 3.5vw, 5.5rem) * var(--k-density)) |
--k-space-8 |
Hero band padding. Fluid. | calc(clamp(4.5rem, 3.3rem + 5.5vw, 8rem) * var(--k-density)) |
Shape
One knob: --k-radius — the small and large steps derive from it, and 0 makes the whole framework square.
| Token | What it does | Default |
|---|---|---|
--k-radius |
Base corner radius: buttons, code blocks, alerts, images, fieldsets. | 8px |
--k-radius-s |
Small step, derived: inline code, inputs, pagination. | calc(var(--k-radius) * 0.5) |
--k-radius-l |
Large step, derived: cards, dialogs. | calc(var(--k-radius) * 1.75) |
Width
| Token | What it does | Default |
|---|---|---|
--k-width-narrow |
Prose measure. Used by .k-container--narrow. |
46rem |
--k-width |
Default container width. Used by .k-container. |
72rem |
--k-width-wide |
Wide container for grids and tables. Used by .k-container--wide. |
90rem |
--k-gutter |
Fluid side margin. Drives the margin-based .k-container--gutter — no width cap, just breathing room. |
clamp(1.25rem, 5vw, 5rem) |
Other
| Token | What it does | Default |
|---|---|---|
--k-shadow-s |
Subtle edge shadow for small controls. | 0 1px 2px hsl(228 35% 12% / 0.05) |
--k-shadow |
The default elevation: cards and dialogs. | 0 1px 2px hsl(228 35% 12% / 0.04), 0 6px 18px -8px hsl(228 35% 12% / 0.10) |
--k-shadow-l |
High elevation: dropdown menus, lifted cards. | 0 2px 6px hsl(228 35% 12% / 0.05), 0 22px 44px -18px hsl(228 35% 12% / 0.20) |
--k-ease |
Duration and easing for every transition. | 160ms ease |
A note on stability: keel is pre-1.0. Token names may still tighten before 1.0 — changes land in the changelog. Treat this list as the current proposal, not a contract.