Foundations
Tokens are the public API. Components churn; token names should not. Everything below is what a consuming app is allowed to depend on - including the theme, which is nothing more than the semantic tier redefined.
Reference
the theme
Light and dark, and no component rule knows about either. The whole theme is one file redefining the semantic tier, written with light-dark() so both values sit side by side in a single declaration rather than in a second palette under [data-theme] and a third under a prefers-color-scheme media query, drifting apart from the first edit. The switch is color-scheme, which is the property the browser already uses to paint scrollbars, form controls and the canvas behind the body - a class that sets the palette without setting color-scheme is how a page ends up dark with a white scrollbar.
<html> <!-- follows the operating system -->
<html data-theme="light"> <!-- pinned light -->
<html data-theme="dark"> <!-- pinned dark -->
<section data-theme="dark"> <!-- one subtree, either way -->| Attribute | Type | Default | Notes |
|---|---|---|---|
data-theme | "light" | "dark" | system | Works on any element, not just the root: light-dark() resolves against the element's own used color-scheme, so a dark hero band on a light page is one attribute. |
@supports | light-dark() | - | The whole theme file is guarded. A browser without light-dark() skips it and keeps the light palette this system shipped with for three versions - the right failure for a page that would otherwise have no colours at all. |
the switcher | - | - | The three links at the bottom of this sidebar are ordinary links carrying ?theme=. Rendering the choice server-side is not a workaround for the no-JavaScript rule: a script-driven toggle cannot run before the first paint, which is what causes the flash of the wrong theme on every page load. |
what it may not do | - | - | Touch a component selector. If a dark value cannot be reached by redefining a token, the component is reaching past the semantic tier - which happened four times while the theme was written, and each one was a latent bug rather than a theming inconvenience. |
sk-mark
The marker-pen swash. Drawn as an inline background rather than a positioned pseudo-element, so it wraps across lines and scales with the text it marks.
<h2>Our <span class="sk-mark">games</span></h2>| Attribute | Type | Default | Notes |
|---|---|---|---|
data-fill | "true" | underline stroke | Fills the whole line box instead of drawing a stroke along the baseline. |
--sk-mark-height | length (em) | 0.4em | Stroke thickness, in em so it holds its proportion at every font size. |
--sk-mark-offset | length (em) | 0.03em | Clearance below the stroke. |
Examples
Colour
kiwi-400 has roughly 1.6:1 contrast against white. It is a fill colour only - never text, never a 1px border on a light surface. Anything sitting on it uses ink (9.4:1).
Brand
kiwi-400 and ink-950 are anchors taken from softkiwi.games and must not drift. The rest of the ramp is derived in OKLCH.
Neutrals
Tinted toward the brand hue so neutral surfaces sit next to kiwi without looking dirty.
Semantic
The only tier components reference. Theming redefines these and nothing else - which is why the dark theme touches no component rule.
Show markup
<div class="sk-stack" data-gap="6">
<div class="sk-stack" data-gap="3">
<h3>
Brand
</h3>
<p class="sink-story-note">
kiwi-400 and ink-950 are anchors taken from softkiwi.games and must not drift. The rest of the ramp is derived in OKLCH.
</p>
<div class="sink-grid">
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="kiwi-100">
</div>
<span class="sink-swatch-name">
--sk-kiwi-100
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="kiwi-200">
</div>
<span class="sink-swatch-name">
--sk-kiwi-200
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="kiwi-300">
</div>
<span class="sink-swatch-name">
--sk-kiwi-300
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="kiwi-400">
</div>
<span class="sink-swatch-name">
--sk-kiwi-400
</span>
<span class="sink-swatch-note">
anchor - bg-kiwi-400
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="kiwi-500">
</div>
<span class="sink-swatch-name">
--sk-kiwi-500
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="kiwi-600">
</div>
<span class="sink-swatch-name">
--sk-kiwi-600
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="kiwi-700">
</div>
<span class="sink-swatch-name">
--sk-kiwi-700
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="kiwi-800">
</div>
<span class="sink-swatch-name">
--sk-kiwi-800
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="ink-950">
</div>
<span class="sink-swatch-name">
--sk-ink-950
</span>
<span class="sink-swatch-note">
anchor - emerald-950
</span>
</div>
</div>
</div>
<div class="sk-stack" data-gap="3">
<h3>
Neutrals
</h3>
<p class="sink-story-note">
Tinted toward the brand hue so neutral surfaces sit next to kiwi without looking dirty.
</p>
<div class="sink-grid">
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="neutral-0">
</div>
<span class="sink-swatch-name">
--sk-neutral-0
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="neutral-50">
</div>
<span class="sink-swatch-name">
--sk-neutral-50
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="neutral-100">
</div>
<span class="sink-swatch-name">
--sk-neutral-100
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="neutral-200">
</div>
<span class="sink-swatch-name">
--sk-neutral-200
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="neutral-300">
</div>
<span class="sink-swatch-name">
--sk-neutral-300
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="neutral-400">
</div>
<span class="sink-swatch-name">
--sk-neutral-400
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="neutral-500">
</div>
<span class="sink-swatch-name">
--sk-neutral-500
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="neutral-600">
</div>
<span class="sink-swatch-name">
--sk-neutral-600
</span>
<span class="sink-swatch-note">
4.9:1 on white
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="neutral-700">
</div>
<span class="sink-swatch-name">
--sk-neutral-700
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="neutral-800">
</div>
<span class="sink-swatch-name">
--sk-neutral-800
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="neutral-900">
</div>
<span class="sink-swatch-name">
--sk-neutral-900
</span>
</div>
</div>
</div>
<div class="sk-stack" data-gap="3">
<h3>
Semantic
</h3>
<p class="sink-story-note">
The only tier components reference. Theming redefines these and nothing else - which is why the dark theme touches no component rule.
</p>
<div class="sink-grid">
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="surface">
</div>
<span class="sink-swatch-name">
--sk-surface
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="surface-sunken">
</div>
<span class="sink-swatch-name">
--sk-surface-sunken
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="surface-raised">
</div>
<span class="sink-swatch-name">
--sk-surface-raised
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="ink">
</div>
<span class="sink-swatch-name">
--sk-ink
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="ink-muted">
</div>
<span class="sink-swatch-name">
--sk-ink-muted
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="ink-subtle">
</div>
<span class="sink-swatch-name">
--sk-ink-subtle
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="border">
</div>
<span class="sink-swatch-name">
--sk-border
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="border-strong">
</div>
<span class="sink-swatch-name">
--sk-border-strong
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="action-bg">
</div>
<span class="sink-swatch-name">
--sk-action-bg
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="action-bg-hover">
</div>
<span class="sink-swatch-name">
--sk-action-bg-hover
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="action-fg">
</div>
<span class="sink-swatch-name">
--sk-action-fg
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="accent">
</div>
<span class="sink-swatch-name">
--sk-accent
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="accent-soft">
</div>
<span class="sink-swatch-name">
--sk-accent-soft
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="danger-fg">
</div>
<span class="sink-swatch-name">
--sk-danger-fg
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="danger-bg">
</div>
<span class="sink-swatch-name">
--sk-danger-bg
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip" data-token="danger-solid-bg">
</div>
<span class="sink-swatch-name">
--sk-danger-solid-bg
</span>
</div>
</div>
</div>
</div>Type scale
Open Sans, self-hosted as a variable font. Two woff2 files cover every weight from 300 to 800.
Show markup
<div>
<div class="sink-row">
<span class="sink-row-label">
--sk-text-xs
</span>
<span class="sink-type" data-token="text-xs">
The quick brown fox
</span>
<span class="sink-swatch-note">
0.75rem
</span>
</div>
<div class="sink-row">
<span class="sink-row-label">
--sk-text-sm
</span>
<span class="sink-type" data-token="text-sm">
The quick brown fox
</span>
<span class="sink-swatch-note">
0.875rem
</span>
</div>
<div class="sink-row">
<span class="sink-row-label">
--sk-text-md
</span>
<span class="sink-type" data-token="text-md">
The quick brown fox
</span>
<span class="sink-swatch-note">
1rem
</span>
</div>
<div class="sink-row">
<span class="sink-row-label">
--sk-text-lg
</span>
<span class="sink-type" data-token="text-lg">
The quick brown fox
</span>
<span class="sink-swatch-note">
1.125rem
</span>
</div>
<div class="sink-row">
<span class="sink-row-label">
--sk-text-xl
</span>
<span class="sink-type" data-token="text-xl">
The quick brown fox
</span>
<span class="sink-swatch-note">
1.25rem
</span>
</div>
<div class="sink-row">
<span class="sink-row-label">
--sk-text-2xl
</span>
<span class="sink-type" data-token="text-2xl">
The quick brown fox
</span>
<span class="sink-swatch-note">
1.5rem
</span>
</div>
<div class="sink-row">
<span class="sink-row-label">
--sk-text-3xl
</span>
<span class="sink-type" data-token="text-3xl">
The quick brown fox
</span>
<span class="sink-swatch-note">
1.875rem
</span>
</div>
<div class="sink-row">
<span class="sink-row-label">
--sk-text-4xl
</span>
<span class="sink-type" data-token="text-4xl">
The quick brown fox
</span>
<span class="sink-swatch-note">
2.25rem
</span>
</div>
</div>Space scale
4px base, eight steps, no half-steps.
Show markup
<div>
<div class="sink-row">
<span class="sink-row-label">
--sk-space-1
</span>
<span class="sink-bar" data-token="space-1">
</span>
<span class="sink-swatch-note">
0.25rem
</span>
</div>
<div class="sink-row">
<span class="sink-row-label">
--sk-space-2
</span>
<span class="sink-bar" data-token="space-2">
</span>
<span class="sink-swatch-note">
0.5rem
</span>
</div>
<div class="sink-row">
<span class="sink-row-label">
--sk-space-3
</span>
<span class="sink-bar" data-token="space-3">
</span>
<span class="sink-swatch-note">
0.75rem
</span>
</div>
<div class="sink-row">
<span class="sink-row-label">
--sk-space-4
</span>
<span class="sink-bar" data-token="space-4">
</span>
<span class="sink-swatch-note">
1rem
</span>
</div>
<div class="sink-row">
<span class="sink-row-label">
--sk-space-5
</span>
<span class="sink-bar" data-token="space-5">
</span>
<span class="sink-swatch-note">
1.5rem
</span>
</div>
<div class="sink-row">
<span class="sink-row-label">
--sk-space-6
</span>
<span class="sink-bar" data-token="space-6">
</span>
<span class="sink-swatch-note">
2rem
</span>
</div>
<div class="sink-row">
<span class="sink-row-label">
--sk-space-7
</span>
<span class="sink-bar" data-token="space-7">
</span>
<span class="sink-swatch-note">
3rem
</span>
</div>
<div class="sink-row">
<span class="sink-row-label">
--sk-space-8
</span>
<span class="sink-bar" data-token="space-8">
</span>
<span class="sink-swatch-note">
4rem
</span>
</div>
</div>Radius
Show markup
<div class="sk-cluster" data-gap="4">
<div class="sink-swatch">
<div class="sink-swatch-chip sink-radius" data-token="radius-sm">
</div>
<span class="sink-swatch-name">
--sk-radius-sm
</span>
<span class="sink-swatch-note">
0.25rem
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip sink-radius" data-token="radius-md">
</div>
<span class="sink-swatch-name">
--sk-radius-md
</span>
<span class="sink-swatch-note">
0.375rem - rounded-md
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip sink-radius" data-token="radius-lg">
</div>
<span class="sink-swatch-name">
--sk-radius-lg
</span>
<span class="sink-swatch-note">
0.5rem - rounded-lg
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip sink-radius" data-token="radius-full">
</div>
<span class="sink-swatch-name">
--sk-radius-full
</span>
<span class="sink-swatch-note">
pills
</span>
</div>
</div>Elevation
Show markup
<div class="sk-cluster" data-gap="5">
<div class="sink-swatch">
<div class="sink-swatch-chip sink-shadow" data-token="shadow-sm">
</div>
<span class="sink-swatch-name">
--sk-shadow-sm
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip sink-shadow" data-token="shadow-md">
</div>
<span class="sink-swatch-name">
--sk-shadow-md
</span>
</div>
<div class="sink-swatch">
<div class="sink-swatch-chip sink-shadow" data-token="shadow-lg">
</div>
<span class="sink-swatch-name">
--sk-shadow-lg
</span>
</div>
</div>The mark
The marker-pen swash from softkiwi.games, promoted to a component. It is the brand's one distinctive gesture, so it gets a token (--sk-mark-height) and a class rather than living in a page stylesheet.
Our games
Inline in a sentence the swash still reads cleanly at body size, because the stroke is measured in em rather than rem - the same class on a heading and on body copy holds the same proportion instead of turning into a strikethrough.
It also wraps, which an absolutely positioned bar cannot do: a marked phrase long enough to run past the end of its line gets a stroke on each fragment rather than one bar drawn across both lines and through the gap.
A full fill when the line needs more weight.
Show markup
<div class="sk-stack" data-gap="5">
<h2>
Our
<span class="sk-mark">
games
</span>
</h2>
<p>
Inline in a sentence the swash still
<span class="sk-mark">
reads cleanly
</span>
at body size, because the stroke is measured in em rather than rem - the same class on a heading and on body copy holds the same proportion instead of turning into a strikethrough.
</p>
<p class="sink-narrow">
It also wraps, which an absolutely positioned bar cannot do:
<span class="sk-mark">
a marked phrase long enough to run past the end of its line
</span>
gets a stroke on each fragment rather than one bar drawn across both lines and through the gap.
</p>
<h2>
A
<span class="sk-mark" data-fill="true">
full fill
</span>
when the line needs more weight.
</h2>
</div>