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).