Skip to content

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

    #ffffff

  • neutral

    #eee8e3 · background

  • neutral-600

    #e2dad2

  • lilac

    #e8e5f6 · secondary

  • blue

    #0e26ac · foreground, accent

  • red

    #b3261e · error

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/color-system.js
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].

Button Secondary

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>
css/base/fonts.css
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
Visual identity by Doug Kerr
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 &amp; 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>
css/tokens/theme.css
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>
postcss/functions.js
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.
css/tokens/globals.css
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>
css/base/elements.css
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.
css/base/reset.css
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.

Budget

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>
css/base/forms.css
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.