The Shared Design System

Learning Website with Next.js

Chapter 5 ยท The Shared Design System

Eight sites should look like one family, and each should be recognisable at a glance. This chapter moves the Django project's theme into a package, @lw/ui, that every app imports: one set of stylesheets, a frame component, and a colour for each site. It answers one real question about Next.js (CSS Modules or global CSS?) and then measures whether the result looks the same as the Django site, instead of saying so.

Run for real, on Next.js 16.4
41 tests pass. The languages and web development apps were built with the design system, served, screenshotted, and a real headless Chrome compared 27 computed styles of one lesson with the Django site. The site is dark only, as the Django one is: there is no light theme.

Tokens: the One Place a Colour Is Decided

tokens.css holds every colour as a variable, and nothing else in the design writes a colour of its own. It is the Django project's file unchanged. Two neutral families exist (a warmer one for the language courses, a cooler one for the technical courses), and each site has one accent. The accent is a CSS variable on the <html> element, so one stylesheet serves every site and only that value differs:

// in each app's layout import "@lw/ui/styles.css"; import { SiteLayout, htmlProps } from "@lw/ui"; <html lang="en-GB" {...htmlProps("languages")}> <body><SiteLayout site="languages">{children}</SiteLayout></body> </html> // the result in the built page <html lang="en-GB" data-site="languages" data-family="language" style="--accent:#a78bfa">
SiteFamilyAccent
Languageslanguage#a78bfa purple
Web Developmenttechnical#38bdf8 sky blue
Programmingtechnical#44b78b green
Systemstechnical#f0938a coral
AI and Datatechnical#fbbf24 amber
Humanitieslanguage#fb923c orange
Life Skillslanguage#6ee7b7 mint
Creative Toolstechnical#f472b6 pink

Rules That Are Tests

  • Every site has a style, and its own accent.
  • Every accent is readable as text on both neutral surfaces: a contrast of at least 4.5 (WCAG level AA). The test reads the real colours out of tokens.css instead of copying them into the test, so changing a token changes what the test sees. The contrast function itself is checked against known values (black on white is exactly 21; #777777 on white is 4.48, the famous “just fails”).
  • No !important in any shared stylesheet.
  • Shared component rules stay weaker than a fragment's own (no ids, at most two parts), explained below.

CSS Modules or Global CSS?

CSS Modules rename every class to something unique, so one can never clash with another. That is exactly right for the site's own parts and exactly wrong for the classes the lesson fragments use. A fragment is a file of HTML written long ago, and it says class="tip-box". If tip-box were a module class, the stylesheet would hold a renamed class and the fragment would match nothing. So the choice follows from one fact: who writes the class name.

Styles forKindWhy
The site's frame: header, footer, skip linkCSS ModuleOnly the site's own code names them; nothing in a fragment ever does
.tip-box, .code-block, tables, .page-titleGlobal CSSFragments name them, so the names must not change

The built output shows the difference:

class names in the built page: NMm9mq_bar NMm9mq_footer NMm9mq_name NMm9mq_skip (the module: renamed) selectors in the built stylesheet: .NMm9mq_skip{... :root{ *,:before,:after{ .tip-box,.warn-box,.finding-box{ (global: names unchanged)

A fragment also brings its own scoped <style> (for example .hu-lesson .tip-box { ... }). The shared rules use a single class, so the fragment's rule is more specific and wins. That is deliberate: an old page keeps its look. A test reads components.css and fails if any selector has an id or more than two parts.

Does It Look the Same as the Django Site?

Both were started, and a real headless Chrome, driven through the DevTools protocol, opened the same Hungarian lesson on each and read the computed style of 27 properties: the accent variable, the body, the main column, the page title, a tip box, a dialog card, a vocabulary table, a speaker label.

RunResult
As built26 identical, 0 different, 1 absent on both sites (a link in the page body that this page does not have)
Page background and text tokens each changed by one step24 identical, 2 different: rgb(15, 17, 23) against rgb(15, 17, 24), and 236 against 237
Radius changed from 8 to 10 px and the language surface by one step26 identical, 0 different
The last row is a finding, not a failure
The lesson carries its own scoped stylesheet, which sets its own radius and surfaces, so changing those tokens changes nothing the lesson shows. The shared tokens only reach the page frame and anything a fragment leaves unstyled. So this check proves that the frame and the page-level colours match, and it says nothing about a token that every fragment overrides. Without the middle row (a change the check can see) its first result would be worth much less.

A style guide page (/styleguide, in both apps) shows every shared building block at once. Looked at by eye: the Languages site is purple on the warmer neutral surface, the Web Development site is sky blue on the cooler one, and the warning and finding boxes keep their own colours on both.

What was not verified
Only one lesson page was compared with Django (the other languages and the other sites were not), and only in Chrome. The design is dark only: there is no light theme and no prefers-color-scheme switch. Fonts are the system fonts; font loading is Chapter 8. The menus, breadcrumbs, search and account rules of the Django stylesheet come in the chapters that need them.

Hands-On Exercises

Exercise 1

Move the design tokens into a shared package and give each site its own accent through a CSS variable on the html element. Write tests that read the real stylesheet: every accent readable on both surfaces, no two the same, every site styled.

๐Ÿ“„ View solution
Exercise 2

Decide which styles are CSS Modules and which are global, from the fact of who writes the class name. Build the site frame as a module, keep the fragment-facing classes global, and show the difference in the built output. Guard the specificity rule with tests.

๐Ÿ“„ View solution
Exercise 3

Measure whether a real lesson looks the same on the Django site and the Next.js site by comparing computed styles in a real browser. Show that your check can fail, and say what it cannot see.

๐Ÿ“„ View solution

Chapter 5 Quick Reference

  • @lw/ui: styles.css (tokens, base, components), SiteLayout, htmlProps(site), StyleGuide, contrast helpers
  • Colours live only in tokens.css; each site's accent is --accent on <html>, from SITE_STYLE in the site package
  • Two neutral families (language, technical); eight accents, all different, all at least 4.5 contrast on both surfaces
  • Modules for the site's own frame; global CSS for classes that fragments write (tip-box, code-block, tables)
  • Built proof: module classes become NMm9mq_bar; .tip-box keeps its name
  • Shared rules: one class each, no ids, no !important, so a fragment's own scoped rule wins
  • Parity with Django: 26 of 26 comparable styles identical; the check fails on a one-step colour change; it cannot see tokens that fragments override
  • Dark only; no light theme; system fonts; only one lesson and Chrome compared