Token layer
Every visual decision in Kanto resolves to a CSS custom property declared on :root. Components never hard-code a colour, a size or a duration.
Every visual decision in Kanto resolves to a CSS custom property declared on
:root. Components never hard-code a colour, a size or a duration.
Custom properties inherit through shadow roots, which is what makes this work
for web components: an app can restyle every Kanto element by redefining a
token on :root, without piercing a single shadow boundary.
Files
| File | Contents | Optional |
|---|---|---|
colors.css | Dark palette, surfaces, text ramp, aliases | no |
theme-light.css | Light remap of the same names | no |
typography.css | Families and composite font shorthands | no |
spacing.css | Radius, padding, gap, dimensions, motion, z | no |
responsive.css | Breakpoint overrides + reduced-motion | no |
fonts.css | @font-face for Manrope / Mulish / SCP | yes |
base.css | Global element styles for the host application | yes |
index.css | Imports the five non-optional files | — |
Usage
or, to keep Kanto out of the host's global styles:
Theming
Dark is the canonical theme and is what :root carries.
Accent colour
Kanto ships violet. Seven presets are a data attribute away, on the page or on any container:
violet · blue · teal · green · orange · pink · slate
Any other colour goes through setAccent, which solves the variants for it:
Whatever the colour, white text reads on a primary fill at 4.5:1 or more, and the accent as text reads at 4.5:1 or more on the surfaces, on its own tint and on the hover wash of a secondary button, in both themes. A bright colour — a yellow, a light orange — is darkened for it, as Kanto's own violet is. The neutral surfaces take a trace of the accent's hue; a grey accent gives plain greys.
accentPalette(color) returns the values without applying them — to write
them into a stylesheet at build time, for instance. The inputs it sets are
--accent-base, --accent-hover, --accent-text-dark, --accent-text-light,
--accent-wash, --neutral-hue and --neutral-chroma; components never read
them, only the tokens do.
A red or a green accent sits close to the danger and success colours; Kanto does not stop you, but a primary button and a destructive one will look alike.
Appearance
Besides the accent, four settings change how Kanto looks. Each is a data attribute, on the page or on any container:
| Attribute | Values |
|---|---|
data-font | kanto · system · inter · plex · geist |
data-radius | sharp · default · round |
data-density | compact · default · comfortable |
data-text-size | small · default · large |
data-theme | dark · light · auto |
Or all at once, with the accent:
setAppearance changes only the settings it is given, and checks them all
first: an unknown value throws and changes nothing.
Density scales the heights of controls and fields — a button is 34px in
compact, 46px in comfortable, on even pixels — the padding and gap
tokens, and table cells. Text size scales every type token and table text.
Both compose with the mobile and ultra-wide scales. A few inner sizes —
calendar cells, the small segmented control — keep their own.
The rounding of heights uses CSS round() where the browser has it (Chrome
125, Firefox 118, Safari 15.4); before that, heights keep their exact
fraction and nothing else changes.
Kanto ships its own faces only. With inter, plex or geist, load the font
yourself (Google Fonts has all three); without it, the system face shows.
The inputs behind the attributes are --radius-scale, --density-scale,
--text-scale, --font-body and --font-display. Set in CSS, they need one
of the attributes on the same element for the tokens to re-read them there;
--font-body and --font-display take a full font stack, the scales a plain
number.
An element carrying one of these attributes re-declares Kanto's spacing and
type tokens from these inputs, so an app that overrides a token itself —
:root { --button-height: 36px } — repeats the override for that element, or
overrides the input instead. The same goes for colour tokens under
data-accent.
Conventions
- Surfaces are a ramp, not a shadow scale. Elevation means a lighter
surface:
--color-dark-12(page) →--color-dark-16(card) →--color-dark-20(raised). Shadows are kept for what floats over the page: toasts, and popovers in the light theme. - Popovers have their own surface. Lists, menus and calendars that open
over the page use
--surface-popover,--border-popover,--shadow-popoverand, for lines inside them,--divider-popover. In the dark the ramp sets them apart alone — a lighter surface, no edge, no shadow. In the light a darker step reads as a grey smudge and fades what sits on it, and white alone is lost on a near-white page: white, then, with an edge and a soft shadow. - Semantic colours come in four variants.
-baseis opaque, for solid fills;-softis the same hue at 12% alpha, for tinted backgrounds;-hoveris 16%;-textis for text and icons. Never colour text with-base: it is tuned to carry white text, which makes it too dark on a dark surface and too light on a light one. Every-textholds WCAG AA (4.5:1) on the page, card and raised surfaces and on its own tint.--color-danger-solidis the red for a filled destructive button. - Prefer the aliases.
--surface-cardover--color-dark-16,--text-mutedover--color-text-400. The aliases survive a change to which ramp step a role uses. - Type is applied whole.
font: var(--font-normal-medium)sets weight, size, line height and family in one declaration; there is no--font-size-*scale to get half-applied. - Inputs take an outline, never a border. A border would reflow the field on
focus;
--outline-widthsits outside the box.