Skip to content
How much to read

Knowledge base

projects / easemysemester / features

v2 design system, tokens and specimen

v2 built a token-based design system, palette drawn from the corpus's own atlas material rather than a brand, with a drift-detecting tokens page, then applied it across every screen.

feature · current · updated 2026-09-15 · EaseMySemester case study

See overview and v1's shadcn/ui migration, which this replaces for v2.

What it does

Colour, type and spacing live as CSS custom properties in one file, globals.css, the only place a literal colour is written. Each colour is defined once as a light or dark literal, and three theme blocks (light, dark, and unset system default) map those onto semantic tokens, so a palette change is one edit and the three states cannot drift apart. Type is a scale of named-role steps sized in rem, so a reader with an enlarged system text size gets a bigger page, and prose has a measure capped at 66ch.

The palette is drawn from the atlas plates the students already study: carmine as the accent, haematoxylin for citations and provenance, ochre for an honest gap, a near-black violet-biased ink. Dark theme lifts the carmine rather than reusing the light hex, since full-strength carmine disappears against a dark ground.

A tokens page renders every token in both themes and stays in the app as a drift detector: a test reads the real stylesheet and fails if the token inventory and the CSS disagree, or if the light, dark and system-dark blocks map different token sets.

Pairing and rollout

Three specimen pairings were built on a shared page, reading real content from the database rather than lorem, so the choice could be made against actual rows: a ranked question, an answer in both forms, a citation, a wrapping heading. Pairing A was locked from a mobile design bundle: Newsreader for questions and screen titles, IBM Plex Sans for prose and controls, IBM Plex Mono for data, so a reader can tell what the corpus measured from what the app wrote. The light palette was redrawn against the bundle rather than derived from the dark palette; nine of ten light tokens changed value, because a light-mode accent has to be chosen against a near-white ground, not lifted from the dark hex. The design system was then applied across every existing surface in the same work.

Component primitives are unstyled Radix, styled entirely from tokens, built as surfaces need them rather than as a speculative kit; this was chosen specifically instead of shadcn, whose v1 mistake was shipping its defaults untouched. One primitive was built ahead of its callers: a band chip (every year / most years / some years), since the mobile screens put it on nearly every row.

A test file, colour-guard.test.ts, sweeps every source file for stock Tailwind colour utilities and raw colour literals and fails the build on a hit, and separately asserts every foreground token clears 4.5:1 contrast against both page and card grounds in both themes; both checks were proven by deliberately injecting a violation and watching them fail.

A bug the tokens page did not catch

--color-ground: var(--ground) was declared on :root, so its value was substituted and inherited already-resolved; a subtree that redefined --ground could not change what it had already inherited, and a dark panel kept the page's background while its ink switched. Found by looking at the rendered page rather than by the automated drift check. A rawThemeVar helper now lets hand-written CSS and inline styles reach the live custom property instead of a resolved value.

Status

Shipped 2026-09-09 (tokens and specimen, then locked pairing and full rollout), same burst as the eight-screen mobile PWA build (details).

This page was written by a model from my own repositories and checked by a second one against the same sources. I did not write it, and no version of it was approved by me before it went up. How that works, and where it can still be wrong.