Skip to content

Appearance

Theme
Mode

Style guide: tokens, type, and contrast →

Style guide

Every color on this site comes from a token, and every token pair is measured. Switch themes or modes and the numbers below update — including the ones that would fail. Components are rendered from the same library the site is built from, so nothing here is a screenshot.

Theme

Three themes across light and dark, applied with data-theme and data-mode on <html>. Each is the same token names with different values, which is what makes the high-contrast theme a value swap rather than a rewrite. The preference is a cookie, read by a script before first paint — no browser storage, no flash.

Theme
Mode

Color

Colors are not a flat palette. Every surface has a paired foreground, so a component written as bg-card text-card-foreground is legible in every theme without anyone remembering which color goes where. Values below are read from the running page.

Surface and foreground pairs

  • PageAa
    --ck-background
    —
    --ck-foreground
    —
  • CardAa
    --ck-card
    —
    --ck-card-foreground
    —
  • MutedAa
    --ck-muted
    —
    --ck-muted-foreground
    —
  • PrimaryAa
    --ck-primary
    —
    --ck-primary-foreground
    —
  • AccentAa
    --ck-accent
    —
    --ck-accent-foreground
    —

Structure

  • Border

    --ck-border

    —

    Decorative hairline

  • Input

    --ck-input

    —

    Control boundary · 3:1

  • Ring

    --ck-ring

    —

    Focus indicator · 3:1

Contrast

Contrast is measured between a surface and the text or icon drawn on it, not between individual colors. WCAG AA requires 4.5:1 for body text and 3:1 for large text. AAA requires 7:1. The high-contrast theme is built to clear AAA on every pair.

Measuring the current theme…

The same math runs at build time in npm run check:contrast, reading globals.css directly, so this page and the gate cannot disagree. Note that --ck-border is not held to 3:1: WCAG 1.4.11 governs control boundaries, not decorative hairlines, which is why --ck-input exists separately.

Typography

Archivo carries display type — headings and the name. IBM Plex Mono carries everything else: body copy, labels, tables, UI. The inversion of the usual serif-on-cream portfolio is deliberate.

Display · Archivo 600

Design systems and the standards behind them

Heading · Archivo 600

VimUI: Vimocity’s design system, built in code

Body · IBM Plex Mono 400

I design products and write the frontend code they’re built from.

Label · IBM Plex Mono 400, uppercase, 0.14em

Selected work

Motion

Three durations and one easing curve, read from the running page. Motion is set once for the whole system, so it stays consistent, and honoring prefers-reduced-motion is a single rule instead of a promise repeated in every component.

Duration scale

  • Fast

    —

    --ck-duration-fast

    Hover and focus. Anything answering a pointer that is still moving.

  • Base

    —

    --ck-duration-base

    State changes you look at: a card’s glow, a skeleton handing off.

  • Slow

    —

    --ck-duration-slow

    Entrances, where content travels furthest.

Easing

—

Steep early, settling late: things move off quickly and arrive gently, so the interface answers at once without lurching. The dashed line is linear, for comparison.

Reduced motion

Off — this browser accepts motion

When this is on, every animation on this site jumps to its finished state: the underline is drawn, the card is visible, and the skeleton hands off without sliding. Content never depends on an animation to become readable.

The typewriter in the hero matters most. Under reduced motion it never starts, because a timer firing forty times a second is still motion, even when nothing appears to move.

Component playground

cksUI — this site’s component library. Built on shadcn/ui’s patterns as source copied in and owned, not as an installed dependency, with every value rewritten onto the tokens above. Every component declares a data-slot, the same convention VimUI uses. The header, footer, skip link, theme switcher, and inspector aren’t repeated as specimens: each is live on this page, and a second copy would fight the first.

Try the inspector

Hit Inspect in the header and point at anything on this page — or tab through it, which works the same way. It reports the component, the tokens its rendered values resolve back to, and the rule behind them.

Values are read with getComputedStyle and resolved backwards to token names, so it reports on the token layer rather than dumping CSS. Anything resolving to no token is a violation of the rule in CLAUDE.md, and the panel says so rather than hiding it.

Component
Variant
Size
State

Rendered

Source

<Button>Read case study</Button>

Button

data-slot="button"

Four variants, each pairing a surface with its foreground so every one stays legible in all three themes.

  • The 44px minimum height is enforced in the component, not remembered at call sites.
  • asChild renders the child element instead of a <button>, for wrapping a link.

The source panel is not a code sample kept beside a demo. Each specimen is one tree, projected twice — once through createElement into the components on the left, once through a printer into the JSX on the right. There is no way to write code here that renders something else, which is the same reason the contrast table reads globals.css instead of keeping its own palette.

Rules

The standard everything above is held to. Each rule is written down for people in CLAUDE.md, loaded into the coding agent from the same file, and checked by a machine before anything merges — so it holds whether a person or an agent wrote the change.

Colors come from tokens
ck/no-raw-color
No hex, rgb, or palette colors in markup. A raw value is the one place a theme cannot reach.
Sizes come from the scale
ck/no-arbitrary-px
No pixel values written into a class. A one-off number is how two components end up a pixel apart.
Every surface is drawn with a measured text color
ck/paired-surface
Any foreground will do, as long as the pair is one the contrast check measures. New tokens are not invented just to make a pair.
Every pair clears WCAG AA
npm run check:contrast
AAA in the high-contrast theme. Measured in all three themes and both modes, from the same file the site is styled with.
Components say what they are
ck/require-data-slot
Every library component sets data-slot, so the inspector can name it and a reviewer can find it.
Words live in the dictionary, not the markup
ck/no-literal-copy
Pages read copy from the locale file; library components take it as props. That is what makes a translation a new file rather than a rewrite.
Markup is accessible
jsx-a11y/strict
Semantic elements, keyboard-operable controls, real labels. The strict rule set, with no rules switched off to pass.
One spelling, one number, nothing internal
npm run check:copy
House spellings and repeated figures stay consistent, stock phrasing is flagged, and nothing private reaches this public repository: no ticket keys, workspace links, or email addresses.
Every page scores 100
npm run lighthouse
Accessibility, best practices, and SEO on Lighthouse’s mobile profile, on every pull request.

What a machine cannot judge is a checklist the design-review skill walks through before a change ships: whether a token is the right one, whether a layout holds at a breakpoint, and whether the empty and error states exist. The ux-writing skill does the same for copy: voice, length, and one name per thing.

Read the rules and the lint plugin on GitHub →