Foundations
Tokens, shared knobs and element defaults: what every page gets before a single class.
css/tokens/ · css/base/ · colors.config.json
Colour
The palette, its roles and the dark scheme are one JSON file,
colors.config.json. A PostCSS plugin turns it into tokens and
surface-* classes, and warns about weak contrast. Why:
colour contexts.
Palette and roles
Each colour comes as a pair, --color-{name} and
--color-{name}-foreground. Roles are aliases of palette colours, and
they're what patterns read: background, foreground,
accent, secondary and error.
white
neutral
neutral-600
lilac
blue
red
Markup
<ul class="grid-auto list-reset [--grid-auto--min-inline-size:--spacing(160)]">
<li class="surface-white swatch"><p class="type-label-sm">white</p><p class="type-meta">#ffffff</p></li>
<li class="surface-neutral swatch"><p class="type-label-sm">neutral</p><p class="type-meta">#eee8e3 · background</p></li>
<li class="surface-neutral-600 swatch"><p class="type-label-sm">neutral-600</p><p class="type-meta">#e2dad2</p></li>
<li class="surface-lilac swatch"><p class="type-label-sm">lilac</p><p class="type-meta">#e8e5f6 · secondary</p></li>
<li class="surface-blue swatch"><p class="type-label-sm">blue</p><p class="type-meta">#0e26ac · foreground, accent</p></li>
<li class="surface-red swatch"><p class="type-label-sm">red</p><p class="type-meta">#b3261e · error</p></li>
</ul>
PostCSS colour system.
Reads colors.config.json and appends to the Tailwind entry stylesheet:
@theme static { --color-*: initial; --color-{name}; --color-{name}-foreground; … }
@utility surface-{name} background, foreground, color-scheme and the colour's vars
@utility on-{name} foreground and vars, no background
@custom-variant dark follows the scheme (OS preference or [data-scheme])
@layer base { … } dark/light role maps for schemes.dark
@source inline(…) every surface-/on- class, only when `safelist` is on
It also warns when a colour and its foreground fall below 4.5:1 contrast.
Plain ESM with JSDoc so it can be copied into any build.
@typedef {{
color?: string,
alias?: string,
name?: string,
foreground?: string,
colorScheme?: 'light' | 'dark',
vars?: Record<string, string>,
}} ColorEntry
@typedef {{
colors: Record<string, ColorEntry>,
schemes?: { dark?: Record<string, string> },
}} ColorConfig
@typedef {{
config?: string | ColorConfig,
safelist?: boolean,
minContrast?: number,
classes?: { surface?: string[], on?: string[] },
}} ColorSystemOptions
`classes` sets the class names generated per colour, with {name} as the colour.
Defaults to surface-{name} and on-{name}; the WordPress adapter adds
has-{name}-background-color and has-{name}-color.
Surfaces
Each colour gives a surface-* class: background, foreground,
color-scheme and the role tokens it sets. Buttons, links, tags, rules
and focus rings read the roles, so they follow the surface with no overrides.
Markup
<ul class="grid-auto list-reset">
<li class="surface-neutral swatch">
<p class="type-label">surface-neutral</p>
<p><a href="#surfaces">Inline link example</a></p>
<p class="flex-list"><a class="btn" href="#surfaces">Button</a> <span class="tag">Tag</span></p>
</li>
<li class="surface-white swatch">
<p class="type-label">surface-white</p>
<p><a href="#surfaces">Inline link example</a></p>
<p class="flex-list"><a class="btn" href="#surfaces">Button</a> <span class="tag">Tag</span></p>
</li>
<li class="surface-lilac swatch">
<p class="type-label">surface-lilac</p>
<p><a href="#surfaces">Inline link example</a></p>
<p class="flex-list"><a class="btn" href="#surfaces">Button</a> <span class="tag">Tag</span></p>
</li>
<li class="surface-blue swatch">
<p class="type-label">surface-blue</p>
<p><a href="#surfaces">Inline link example</a></p>
<p class="flex-list"><a class="btn" href="#surfaces">Button</a> <span class="tag">Tag</span></p>
</li>
<li class="surface-red swatch">
<p class="type-label">surface-red</p>
<p><a href="#surfaces">Inline link example</a></p>
<p class="flex-list"><a class="btn" href="#surfaces">Button</a> <span class="tag">Tag</span></p>
</li>
<li class="surface-background swatch">
<p class="type-label">surface-background</p>
<p><a href="#surfaces">Follows the scheme</a></p>
<p class="flex-list"><a class="btn" href="#surfaces">Button</a> <span class="tag">Tag</span></p>
</li>
</ul>
On a colour
on-* sets the foreground and roles without a background, for text over
an image or a background set some other way.
on-blue
Inline link example and a button
Markup
<div class="img-fit aspect-[3/1] rounded-md">
<div class="bg-blue"></div>
<div class="on-blue p-24 stack-16">
<p class="type-h3">on-blue</p>
<p><a href="#on-colour">Inline link example</a> and a <a class="btn btn--small" href="#on-colour">button</a></p>
</div>
</div>
Dark scheme
Roles switch with the OS setting, or with data-scheme on any element.
Kite Co.'s dark scheme is the blue. This box is always dark:
Text, an inline link example and buttons inside [data-scheme=dark].
Markup
<div data-scheme="dark" class="p-24 rounded-md stack-16">
<p>Text, an <a href="#dark-scheme">inline link example</a> and buttons inside <code>[data-scheme=dark]</code>.</p>
<p class="flex-list">
<a class="btn" href="#dark-scheme">Button</a>
<a class="btn btn--secondary" href="#dark-scheme">Secondary</a>
<button class="btn btn--ghost" type="button">Ghost</button>
</p>
</div>
Type
Sizes are theme tokens (--text-h1 …), each carrying its line height and
letter spacing. Most are fluid: resize the window to see them scale between the 400 and
1440 pixel frames. The type styles pair them with a
family and weight.
Fonts
- --font-serif
- IBM Plex Serif Medium, for headings and captions
- --font-sans
- IBM Plex Sans, for body text
- --font-mono
- IBM Plex Mono, for labels and tags
Markup
<dl class="stack-16">
<div class="stack-4">
<dt class="type-label-sm">--font-serif</dt>
<dd class="font-serif font-medium text-h3">IBM Plex Serif Medium, for headings and captions</dd>
</div>
<div class="stack-4">
<dt class="type-label-sm">--font-sans</dt>
<dd class="font-sans text-h3">IBM Plex Sans, for body text</dd>
</div>
<div class="stack-4">
<dt class="type-label-sm">--font-mono</dt>
<dd class="font-mono font-semibold text-h3">IBM Plex Mono, for labels and tags</dd>
</div>
</dl>
Fonts
The IBM Plex family, self-hosted from public/fonts/ and served at /fonts/
(Latin subsets from Google Fonts; SIL Open Font License, see OFL.txt).
Families are wired to the theme in tokens/theme.css:
--font-serif IBM Plex Serif Medium headings, captions
--font-mono IBM Plex Mono SemiBold labels, nav, tags, buttons
(Regular for code)
--font-sans IBM Plex Sans body
The URLs are root-absolute because imports are flattened before url()s
resolve, so a relative path would resolve from the entry file.
Type scale
- type-hero · fluid(36, 56)
- Cultural organisations
- type-h1 · fluid(32, 42)
- Cultural organisations
- type-h2 · fluid(28, 32)
- Cultural organisations
- type-h3 · fluid(24, 28)
- Cultural organisations
- type-h4 · fluid(20, 24)
- Cultural organisations
- type-h5 · fluid(18, 20)
- Cultural organisations
- type-h6 · 16
- Cultural organisations
- type-base · 17
- Better technology and better ways to communicate.
- type-label · 16
- Announcement
- type-label-sm · 14
- CMS & Content Systems
- type-meta · 14
- type-code · 13
- fluid(28, 36)
Markup
<dl class="grid-simple cols-1 md:cols-2 [--row-gap:--spacing(24)]">
<div class="stack-8">
<dt class="type-label-sm">type-hero · fluid(36, 56)</dt>
<dd class="type-hero">Cultural organisations</dd>
</div>
<div class="stack-8">
<dt class="type-label-sm">type-h1 · fluid(32, 42)</dt>
<dd class="type-h1">Cultural organisations</dd>
</div>
<div class="stack-8">
<dt class="type-label-sm">type-h2 · fluid(28, 32)</dt>
<dd class="type-h2">Cultural organisations</dd>
</div>
<div class="stack-8">
<dt class="type-label-sm">type-h3 · fluid(24, 28)</dt>
<dd class="type-h3">Cultural organisations</dd>
</div>
<div class="stack-8">
<dt class="type-label-sm">type-h4 · fluid(20, 24)</dt>
<dd class="type-h4">Cultural organisations</dd>
</div>
<div class="stack-8">
<dt class="type-label-sm">type-h5 · fluid(18, 20)</dt>
<dd class="type-h5">Cultural organisations</dd>
</div>
<div class="stack-8">
<dt class="type-label-sm">type-h6 · 16</dt>
<dd class="type-h6">Cultural organisations</dd>
</div>
<div class="stack-8">
<dt class="type-label-sm">type-base · 17</dt>
<dd>Better technology and better ways to communicate.</dd>
</div>
<div class="stack-8">
<dt class="type-label-sm">type-label · 16</dt>
<dd class="type-label">Announcement</dd>
</div>
<div class="stack-8">
<dt class="type-label-sm">type-label-sm · 14</dt>
<dd class="type-label-sm">CMS & Content Systems</dd>
</div>
<div class="stack-8">
<dt class="type-label-sm">type-meta · 14</dt>
<dd class="type-meta">Visual identity by Doug Kerr</dd>
</div>
<div class="stack-8">
<dt class="type-label-sm">type-code · 13</dt>
<dd class="type-code">fluid(28, 36)</dd>
</div>
</dl>
Design tokens
Everything here is a Tailwind theme variable, so each namespace also gives
utilities (text-h1, ease-out-quad, max-w-md …). Names follow Tailwind.
Colours come from colors.config.json via postcss/color-system.js, which
also clears Tailwind's default palette (--color-*: initial).
Space and size
One spacing unit, a handful of shared knobs, and fluid values built at compile time. Why: spacing and fluid sizing.
Spacing
--spacing is 1px at the default root size, so a spacing number is a
pixel size in rem: p-16 is 1rem. In CSS, write
--spacing(16). Use px only for things that shouldn't
scale with text, like hairlines.
- 8
- 16
- 24
- 32
- 48
- 64
- 96
- 128
Markup
<dl class="stack-8">
<div class="flex items-center gap-16"><dt class="type-label-sm w-48">8</dt><dd class="h-12 w-8 bg-accent"></dd></div>
<div class="flex items-center gap-16"><dt class="type-label-sm w-48">16</dt><dd class="h-12 w-16 bg-accent"></dd></div>
<div class="flex items-center gap-16"><dt class="type-label-sm w-48">24</dt><dd class="h-12 w-24 bg-accent"></dd></div>
<div class="flex items-center gap-16"><dt class="type-label-sm w-48">32</dt><dd class="h-12 w-32 bg-accent"></dd></div>
<div class="flex items-center gap-16"><dt class="type-label-sm w-48">48</dt><dd class="h-12 w-48 bg-accent"></dd></div>
<div class="flex items-center gap-16"><dt class="type-label-sm w-48">64</dt><dd class="h-12 w-64 bg-accent"></dd></div>
<div class="flex items-center gap-16"><dt class="type-label-sm w-48">96</dt><dd class="h-12 w-96 bg-accent"></dd></div>
<div class="flex items-center gap-16"><dt class="type-label-sm w-48">128</dt><dd class="h-12 w-128 bg-accent"></dd></div>
</dl>
Fluid values
fluid(min, max) becomes a clamp() that scales between the
preset's viewport range, 400 to 1440 here. Sizes are unitless pixels.
--gutter: fluid(20, 56)
Markup
<div class="stack-8">
<p class="type-label-sm">--gutter: fluid(20, 56)</p>
<div class="h-12 w-(--gutter) bg-accent"></div>
</div>
Build-time CSS functions, run through postcss-functions.
to-rem(24) → 1.5rem
to-em(24) → 1.5em
to-em(24, 12) → 2em
fluid(28, 36) → clamp(1.75rem, 1.5682rem + 0.9091vw, 2.25rem)
transition(color, opacity)
→ color var(--default-transition-duration) var(--default-transition-timing-function), …
All sizes are written as unitless px (a trailing "px" is allowed). Use Tailwind's
--spacing(n) for spacing; these cover what --spacing() can't.
Plain ESM with JSDoc so it can be copied into any build.
Global knobs
Custom properties that several patterns read, declared on :root:
--gutter- Inline space between the viewport edge and content, fluid(20, 56).
--space-base,--space-layout- Vertical rhythm: 16 between text elements; 32, then 64 from sm, between blocks.
--gap- The default gap for grids, fluid(8, 12).
--focus-width,--focus-offset,--rule-width- The focus ring and keylines, in px because they shouldn't scale.
Global knobs
Shared custom properties that several patterns read. Single-dash names, like
Tailwind's. Anything owned by one pattern lives with that pattern instead.
Not declared here on purpose (read with a fallback, so a context can set them):
--focus-color falls back to --color-foreground
--rule-color falls back to --color-foreground at half strength
--col-gap, --row-gap fall back to --gap
Containers and breakpoints
Tailwind's container scale is replaced with the widths the layout uses. Breakpoints are Tailwind's, plus three more.
- container-2xs
- 560
- container-xs
- 720
- container-sm
- 765, layout-narrow
- container-md
- 1200
- container-lg
- 1328, layout-wide
- 2xs
- 400
- xs
- 576
- sm · md · lg
- 640 · 768 · 1024
- xl · 2xl
- 1280 · 1536
- 3xl
- 1900
Markup
<div class="grid-simple cols-1 md:cols-2 [--gap:--spacing(32)]">
<dl class="stack-8">
<div class="flex justify-between gap-16"><dt class="type-label-sm">container-2xs</dt><dd>560</dd></div>
<div class="flex justify-between gap-16"><dt class="type-label-sm">container-xs</dt><dd>720</dd></div>
<div class="flex justify-between gap-16"><dt class="type-label-sm">container-sm</dt><dd>765, layout-narrow</dd></div>
<div class="flex justify-between gap-16"><dt class="type-label-sm">container-md</dt><dd>1200</dd></div>
<div class="flex justify-between gap-16"><dt class="type-label-sm">container-lg</dt><dd>1328, layout-wide</dd></div>
</dl>
<dl class="stack-8">
<div class="flex justify-between gap-16"><dt class="type-label-sm">2xs</dt><dd>400</dd></div>
<div class="flex justify-between gap-16"><dt class="type-label-sm">xs</dt><dd>576</dd></div>
<div class="flex justify-between gap-16"><dt class="type-label-sm">sm · md · lg</dt><dd>640 · 768 · 1024</dd></div>
<div class="flex justify-between gap-16"><dt class="type-label-sm">xl · 2xl</dt><dd>1280 · 1536</dd></div>
<div class="flex justify-between gap-16"><dt class="type-label-sm">3xl</dt><dd>1900</dd></div>
</dl>
</div>
Motion
Easing curves in every family (--ease-out-quart …) and a default
transition of 200ms ease-out-quad, which
transition(color, opacity) reads. Every animation stops for reduced
motion.
Elements
Bare elements get the matching pattern, so markup from a CMS or Markdown looks right without classes. They sit in the base layer, so any class wins.
Text
Heading 1
Heading 2
Heading 3
Heading 4
Heading 5
Heading 6
A paragraph with a link to somewhere, strong
and emphasised text, inline code and
small print.
Kite Co. brought both deep technical expertise and genuine care to every stage.
The client
pre: type-code, wrapping
Markup
<div class="flow">
<h1>Heading 1</h1>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h5>Heading 5</h5>
<h6>Heading 6</h6>
<p>
A paragraph with <a href="#text-elements">a link to somewhere</a>, <strong>strong</strong>
and <em>emphasised</em> text, <code>inline code</code> and
<small>small print</small>.
</p>
<blockquote>
<p>Kite Co. brought both deep technical expertise and genuine care to every stage.</p>
<cite>The client</cite>
</blockquote>
<hr />
<pre><code>pre: type-code, wrapping</code></pre>
</div>
Element defaults
Bare elements get the matching pattern, so markup from a CMS or Markdown
looks right without classes. Imported into @layer base, so any class wins.
Reset additions
Tailwind's preflight does most of the reset (box-sizing, margins, media,
form fonts). This file adds what it doesn't. Imported into @layer base.
Forms
Opt in with base/forms.css when the project controls its form markup.
Leave it off when a form plugin brings its own styles, and use the
form control classes instead.
Markup
<form class="max-w-(--container-2xs) stack-24" onsubmit="return false">
<div class="stack-8">
<label class="type-label-sm" for="name">Name</label>
<input id="name" type="text" autocomplete="name" placeholder="Ada Lovelace" />
</div>
<div class="stack-8">
<label class="type-label-sm" for="topic">Topic</label>
<select id="topic">
<option>A new site</option>
<option>Help with an existing one</option>
<option>Something else</option>
</select>
</div>
<div class="stack-8">
<label class="type-label-sm" for="message">Message</label>
<textarea id="message"></textarea>
</div>
<fieldset class="stack-8">
<legend class="type-label-sm">Budget</legend>
<label class="flex items-center gap-8"><input type="radio" name="budget" checked /> Small</label>
<label class="flex items-center gap-8"><input type="radio" name="budget" /> Large</label>
</fieldset>
<label class="flex items-center gap-8"><input type="checkbox" checked /> Send me a copy</label>
<p><button class="btn" type="submit">Send</button></p>
</form>
Form element defaults (opt-in)
Styles bare form elements with the form patterns. Turn it on in index.css
when the project controls its form markup; leave it off when a third-party
form plugin brings its own styles, and use the classes instead.