Skip to content
Kimenpre-v1
Color scheme

Guide

Themes & color schemes

Every visual value in Kimen resolves from a design token — zero hardcoded values, enforced by lint. A theme is nothing but a set of token assignments, so re-theming is a stylesheet plus one attribute, with no component changes and no runtime code.

Tokens are layered (the Design tokens guide covers each layer in depth):

  1. Primitive — raw ramps (--ki-color-brand-500, --ki-space-md). Never consumed by components directly.
  2. Semantic — meaning, not appearance (--ki-surface-s0…s5, --ki-text-high-em, --ki-outline-low-em). This is the layer a brand reassigns to re-theme everything.
  3. Component — the public styling contract of each element (--ki-button-primary-neutral-rest-bg, --ki-button-md-height). Each component page lists its own under CSS custom properties.

Onmars is the default theme — it is tokens.css itself, no attribute needed. Material 3 ships as the reference second theme and proves in CI, on every commit, that reassigning the token layers alone restyles every component:

<link rel="stylesheet" href="@kimen/tokens/css" />
<!-- opt-in second theme -->
<link rel="stylesheet" href="@kimen/tokens/css/material3" />
<html data-ki-theme="material3">

An unknown data-ki-theme value simply matches no selector: the document stays on the default theme.

Both themes follow the OS by default and can be forced either way:

<html data-ki-color-scheme="dark"> <!-- force dark -->
<html data-ki-color-scheme="light"> <!-- force light -->
<!-- no attribute: follows prefers-color-scheme -->

This site’s theme toggle drives exactly this attribute, so every live demo on these pages follows it.

A token stylesheet can only carry custom properties, so it cannot declare color-scheme. Without that declaration the tokens flip to their dark values for a visitor who prefers dark while the browser keeps painting a light canvas, light scrollbars and light autofill — dark Kimen surfaces on a white page.

Load the opt-in page contract alongside a theme to close that gap. It declares color-scheme, paints the page from --ki-surface-s0 / --ki-text-high-em, and sets the body family:

<link rel="stylesheet" href="@kimen/tokens/css" />
<link rel="stylesheet" href="@kimen/tokens/css/base" />

It publishes no tokens of its own — it only consumes them, so it never widens the token surface. If your page is deliberately single-scheme, set data-ki-color-scheme and both the tokens and the browser follow that choice.

Skipping it is supported in the light scheme. In the dark scheme the number is measured rather than estimated: on a page loading only the token stylesheet, with the visitor’s OS set to dark, 14 of 29 components render text that falls below 4.5:1 — most of it at 1.00:1, white on white. Nothing is broken in the components; the user agent simply keeps a light canvas because color-scheme is still normal. So: load the contract, or pin the page with data-ki-color-scheme="light". Doing neither is the one combination the library cannot render legibly.

The semantic surface scale in the current scheme — each swatch reads its color from the live token, so flipping this site’s theme toggle restyles them:

s0
s1
s2
s3
s4
s5
<div style="display: flex; gap: 0.5rem; flex-wrap: wrap;">
<div style="inline-size: 5.5rem; block-size: 3.5rem; display: grid; place-items: center; border-radius: 0.5rem; border: 1px solid var(--ki-outline-low-em); background: var(--ki-surface-s0); color: var(--ki-text-med-em); font-family: var(--ki-typography-family-mono); font-size: 0.75rem;">s0</div>
<div style="inline-size: 5.5rem; block-size: 3.5rem; display: grid; place-items: center; border-radius: 0.5rem; border: 1px solid var(--ki-outline-low-em); background: var(--ki-surface-s1); color: var(--ki-text-med-em); font-family: var(--ki-typography-family-mono); font-size: 0.75rem;">s1</div>
<div style="inline-size: 5.5rem; block-size: 3.5rem; display: grid; place-items: center; border-radius: 0.5rem; border: 1px solid var(--ki-outline-low-em); background: var(--ki-surface-s2); color: var(--ki-text-med-em); font-family: var(--ki-typography-family-mono); font-size: 0.75rem;">s2</div>
<div style="inline-size: 5.5rem; block-size: 3.5rem; display: grid; place-items: center; border-radius: 0.5rem; border: 1px solid var(--ki-outline-low-em); background: var(--ki-surface-s3); color: var(--ki-text-med-em); font-family: var(--ki-typography-family-mono); font-size: 0.75rem;">s3</div>
<div style="inline-size: 5.5rem; block-size: 3.5rem; display: grid; place-items: center; border-radius: 0.5rem; border: 1px solid var(--ki-outline-low-em); background: var(--ki-surface-s4); color: var(--ki-text-med-em); font-family: var(--ki-typography-family-mono); font-size: 0.75rem;">s4</div>
<div style="inline-size: 5.5rem; block-size: 3.5rem; display: grid; place-items: center; border-radius: 0.5rem; border: 1px solid var(--ki-outline-low-em); background: var(--ki-surface-s5); color: var(--ki-text-med-em); font-family: var(--ki-typography-family-mono); font-size: 0.75rem;">s5</div>
</div>

When you need to customize a component, reach for surfaces in this order — if a consumer would ever need !important, that is an API bug to report:

  1. Tokens — reassign at :root (theme-wide) or on a subtree.
  2. ::part() — style exposed parts for one-off structural tweaks.
  3. Slots — replace content entirely.