Case study — Design system

The design system that measures what it claims

A design system designed, built, measured and governed single-handedly, where accessibility is not asserted but computed, component by component, on the prototype as rendered in the browser.

A board of DeepFlow components in dark mode, on a near-black ground. Nine raised surfaces each carry a rendered component: the four button variants — violet, white outline, text only, red —, two notifications, one green for confirmation and one red for an alert, an email field with its label and helper text, a dropdown, three checkboxes and radio buttons with two switched on in violet, a content card, four icon buttons, and an orange warning notification.
Role
Foundation design, component design and build, pattern composition, accessibility verification, documentation and governance
Deliverables
Figma library · two-tier variables, light and dark modes · tokens exported in DTCG format · a 17-component catalogue · 2 composed patterns · documentation site · accessibility criteria grid · SemVer governance

Context

The European Accessibility Act has been in force since June 2025. It makes digital accessibility mandatory for a large share of products sold in Europe, and it rests on a precise technical reference, EN 301 549, which incorporates WCAG 2.2. For a supplier bidding on public contracts, this is no longer a desirable quality. It is a clause.

A design system is where that requirement is settled. It fixes the markup, the states, the focus and the contrasts before a single page exists. Done well, it makes compliance free for the teams that use it. Done badly, it makes them pay the same debt again on every screen.

Problem

Most teams treat accessibility as an end-of-pipeline review. You design, you build, then you audit, and you fix whatever can still be fixed.

The problem is not how serious the teams are, it is the order of operations. By audit time the decisions that matter are already made: the element chosen, the way a state is announced, the contrast of the colour set, the structure of the form. Reworking them is expensive, so not all of them get reworked.

A design system can invert that order. It is the one place where compliance is decided once, upfront, for every team that will use it afterwards. It still has to actually hold, and to be able to show it rather than assert it. That is the system I set out to build: foundations that carry the theme, a catalogue built on native elements, patterns that verify the assembly, and documentation that makes the whole thing usable by someone other than me.

Challenge 01 — Tokens that carry the theme, not just the colours

The challenge: build a token foundation that survives a theme change without anyone touching the components again.

My approach: two strict tiers. Primitives are options (a palette, a type scale, radii). Semantic tokens are decisions (surface/background, text/accent, border/control), and only those are used inside components. Light and dark modes live at the semantic tier. The source is Figma, exported in DTCG format to a tokens.css weighing 3.4 KB gzipped, both themes included.

Outcome: the supervision console exists in light and dark. The dark version required no redraw, no re-entered colour, no extra variant. One mode switch, and more than two hundred bindings re-resolve. Contrasts measured after the switch hold AAA, between 9.7:1 and 17.2:1 on text.

A supervision screen in light theme. A navigation bar carries the entries Missions, Fleet, Incidents and Reports. Below, the title "Mission of 2 August 2026" and the time of the last update, then an alert banner reporting the loss of telemetry link with drone 07. Tabs follow, then two filters, and a table of five aircraft giving for each its status, battery, altitude and time of last contact.The same screen in dark theme. Blocks, layout and positions are strictly identical to the previous panel: only the colours change.
The same screen, two themes. No component redrawn: modes live in the semantic tokens.
The library variables panel, "Semantic — Color" collection. Each row is a decision token, with a Light column and a Dark column. Every value in both columns points to a primitive variable: default text targets neutral 900 in light and neutral 50 in dark, the background surface targets neutral 0 in light and neutral 900 in dark. No colour is entered directly.
The two-tier architecture: primitives are options, semantic tokens are decisions.

Challenge 02 — A catalogue built on native elements

The challenge: deliver a complete catalogue with no dependency, no framework, and without frugality being paid for in accessibility.

My approach: enforceable rules, written before the first component. Zero npm dependencies. Zero JavaScript framework. No utility CSS library. No JavaScript where HTML and CSS suffice. No hardcoded colour. Focus always visible and offset. A proposal that breaks a rule must be justified explicitly, or refused.

Those rules produced the most interesting trade-offs in the project. The native <select> beats a custom combobox: you inherit keyboard support, type-ahead, screen reader behaviour and the mobile picker for free, at the cost of an unstylable open list. The native <dialog> with showModal() supplies the focus trap, focus return, the Escape key and an inert backdrop for two lines of JavaScript. The fully clickable card uses a stretched link on a pseudo-element, never a div with a click handler. And when a field border failed the 1.4.11 contrast criterion, I created a reusable border/control token rather than patching a local value: the defect was fixed once, for every component that reused it afterwards.

Outcome: nine of the twelve prototyped components work without a single line of JavaScript. The three that carry some do so for an irreducible reason, and it shows in the numbers: 606 bytes to open a modal, 698 to make a tooltip dismissible, 1,332 to feed a live region. Each component weighs between 1.1 and 2.9 KB gzipped.

The catalogue now holds seventeen components. The last five were designed for a specific domain, that of operations interfaces: data table, status badge, persistent alert banner, tabs and timestamp. They live in the library, with their variants, states and usage descriptions, and they reuse the same foundations as the first twelve.

The Button component inspection page rendered in a browser. The four variants — primary, secondary, tertiary and danger — sit side by side, each in both sizes. Below come the full-width modifier, then the disabled and loading state rows, where the label gives way to a progress icon.
The Button inspection page: four variants, two sizes, six states, plus the full-width modifier. Rendered in the browser, not drawn.
The catalogue page of the documentation site, light theme. Components are grouped by use — actions, input, navigation, containers, feedback, content — and each carries a card with its name, its two grades and its purpose in one sentence. The side menu also lists upcoming components, marked as such.The same page in dark theme. Grouping, cards and menu are identical: only the colours change.
The documented catalogue, ordered by use. The menu also flags what does not exist yet: an honest system says where it stops.

Challenge 03 — Criteria verified in the browser, not declared

The challenge: be able to state precisely what is compliant, against which criteria, with which tool, and on which date. "Our components are accessible" is not an acceptable answer in a tender response.

My approach: a grid of criteria drawn from WCAG 2.2, applied component by component. Each component retains eight to ten, and for each one a ruling is required: applicable, not applicable, or discarded because another criterion already covers it. Visible focus, full keyboard support, touch target size, behaviour at 200% zoom, accessible name and role, contrast of both text and interface elements, announcement of state changes.

Verification happens in a browser, on the rendered prototype, never by static analysis alone: axe DevTools for what can be automated, Lighthouse as corroboration, then keyboard, zoom and VoiceOver for everything else. Automated checkers cover only part of the criteria; the rest is tested by hand, or not tested at all.

Outcome: zero axe-core 4.11.4 violations across the prototyped catalogue, confirmed in the browser and dated. The grid also produces a grade per component, which serves to locate where the system is weak rather than to award itself a label. Only one component falls short of the maximum: the Button, which without JavaScript does not announce its loading state change. The limit is accepted, but it is counted. A grid that had found nothing would have proved nothing.

The Accessibility tab of the Button page on the documentation site. The A 92.6 grade is restated at the top, followed by a table of nine criteria: each carries its wording, its impact — moderate, serious or critical — and its result. Eight read “pass” in green; the seventh, announcing the loading state, reads “fail” in red. A callout below the table names that failure as the accepted limit of shipping zero JavaScript.The docs/grille-a11y.md document: a table of eleven numbered criteria, each with its WCAG 2.2 reference, the matching axe-core rule where one exists — target-size, color-contrast, button-name, image-alt — or the word “manual”, and its weight. Two sections follow: the weighting convention, and the selection discipline, which requires ruling each criterion applicable, not applicable, or discarded as a duplicate.
For each component: the criteria retained, those discarded and why, the test method and the result.

Each component also carries a second grade, for weight, built on three variables measured on the rendered prototype: DOM complexity, own requests, bytes transferred. It is deliberately kept apart from the first. A single indicator would have dissolved the tensions at the exact moment they need to be visible.

A private browsing window. On the left, the Button inspection page as it renders; on the right, the axe DevTools panel opened on that same address. The “Total issues” counter reads 0, and the breakdown confirms it line by line: zero automatically detected, zero guided, zero manual, zero critical, serious, moderate and minor. At the foot of the panel, the two settings of the run: best practices off, WCAG 2.2 AA standard.
Measurement happens on the rendered prototype in an isolated browser, never in the design tool. Re-checked on axe-core 4.12.1: still zero violations.

Challenge 04 — Measuring against the benchmark

The challenge: get out of self-assessment. A grade you award yourself is worth nothing until it has been confronted.

My approach: I rebuilt the same sign-up page, in its error state, twice. Once with DeepFlow components, once in GOV.UK Design System markup, using the real govuk-frontend 6.2.0 bundle. The competitor is chosen against convenience: GOV.UK is the world reference for the accessible form, and it is itself frugal. Beating a heavy React system would have demonstrated nothing.

Outcome: a tie at the top on accessibility, zero axe-core violations on both sides. The difference lies in the cost. To announce errors on load, DeepFlow spends 691 bytes of JavaScript, GOV.UK spends 11,183. Sixteen times less for the same result. The DOM is leaner too (52 nodes against 57), and so is the CSS shipped (11.9 KB against 14.8).

The “Create an account” form in its error state, DeepFlow version. A red “There is a problem” box opens the page and lists two errors as links. The fields follow: title, full name, email address — outlined in red, with its message under the field — password, then the terms and newsletter checkboxes, and the purple “Create my account” button.The same form built with GOV.UK Design System. The error summary sits above the page title, failing fields are marked by a red bar on their left rather than by an outline, and the submit button is green and full width. The content itself is identical to the left-hand panel.
The same page, in its error state, built twice. DeepFlow on the left, the benchmark on the right.

Where I lose is instructive: eleven requests against three, because I ship one stylesheet per component, and CSS that GOV.UK purges better per page. Both levers are identified, quantified, and not applied. They sit in the roadmap, not in the story.

Five measurements compared, each on its own scale: purple bars are DeepFlow, hatched green bars are GOV.UK, and the shorter bar is always the more frugal one. JavaScript: 691 bytes against 11,183 — the widest gap on the board. DOM nodes: 52 against 57. CSS shipped: 11,965 bytes against 14,817, with a note that GOV.UK, purged per page, would drop to about 4,800. HTTP requests: 11 against 3, DeepFlow loses. First-visit weight: 62.8 kB against 27, DeepFlow loses again — but off the gov.uk domain, where GOV.UK has no font to load; on its own domain it rises to about 88 kB. A closing note restates the accessibility tie: zero axe-core violations on both sides.
The full record, gains and losses. A comparison showing only the gains would not be a measurement.

Challenge 05 — From catalogue to product

The challenge: a compliant catalogue does not guarantee a compliant screen. That had to be verified rather than hoped for.

My approach: two composed patterns, at product level. The first is a sign-up page in its error state, because that is where a form's accessibility is actually settled. The second is a supervision console, which puts the five operations components to work alongside the navigation, fields and buttons of the original catalogue.

Outcome: both patterns surfaced requirements that belonged to no component.

On the form, the error summary did not exist in the catalogue. It had to be built at page level. And announcing it turned out to be subtler than expected: a role="alert" placed in the initial HTML is never announced, since it only fires on insertion. The robust solution is to move focus to the summary on load, which requires JavaScript. Without it the page stays compliant and usable; the script only adds the proactive announcement.

Visual to be produced

The five supervision components

Library board showing the five new components with their variants: Status Badge (4 types), Alert (4 types), Tabs (4 active-tab states), Timestamp (3 formats), Table with its sortable header cells. Dark mode preferred, consistent with the cover.

16 : 9

The five operations components designed for the console: table, status badge, alert banner, tabs, timestamp.

On the console, three decisions imposed themselves the same way. Data freshness is displayed, because a screen that refreshes without saying when it last did makes every reading undatable. The critical alert points to the table row instead of duplicating it, so there is only one source of truth. And I refused to put a live region on the status column: it would have announced a change every few seconds and made the screen unusable with a screen reader. That refusal is a design decision, not an oversight.

The supervision console in dark mode, carrying three numbered pins that are picked up and expanded in a column on the right. The first sits after the line “Dernière mise à jour il y a 12 s” under the mission title: a screen that refreshes without saying when it last did makes every reading undatable. The second is inside the red alert banner, at the end of the sentence stating that the aircraft is flagged Critical in the table below: the state lives in one place, the row, and the banner leads to it rather than copying it. The third sits on the “Critique” badge of the Drone 07 row, in the status column: no live region was put there — it would have announced a change every few seconds and made the screen unusable with a screen reader.
What composition demands, and no component carried.

Documenting the system with itself

A design system nobody can consult exists only for the person who wrote it. Documentation is therefore a deliverable of the system, not an appendix.

The site runs to some twenty pages: a getting-started guide, the foundations, a page per component, the composed patterns, the verification method, and an internal search. Each component page gives what it is for, when to use it, its variants, its accessibility signature and the pitfall to avoid. It is generated by a Node build of roughly a hundred lines, with no dependency at all, and its outputs are versioned: it deploys statically, with no build step on the host.

Above all it is built with the system it documents. Its own buttons, fields, cards and navigation bars are the catalogue's, with the same tokens and the same light and dark modes. It is the system's first consumer product, and the first chance to discover what is missing by actually using it.

The Button component page on the documentation site: what it is for, when to use it, its variants rendered live, its accessibility signature and the pitfall to avoid. The side menu lists every foundation and component.The Colours page of the foundations. Each semantic token is shown as a swatch of its real colour, with its name and its values in hexadecimal, RGB, HSL and OKLCH. The colours displayed are read from the system tokens, not redrawn for the documentation.
The documentation site is the system's first consumer product: it is built from its own components.

Governing a system you maintain alone

A design system without versioning rules is a folder of files. I wrote mine before I needed them: a SemVer contract defining what breaks and what is merely an addition, a changelog, a roadmap separating what is done, in progress and planned, and an eleven-point component template applied to all twelve.

On top of that sits a three-level maturity grid, deliberately distinct from the two grades: it measures the engineering rigour around a component, not its delivered quality. The audit is instructive. All twelve prototyped components sit at the middle level, none reaches the top, and the three blockers are identical for all of them: internationalisation unverified, regression tests absent, user testing with disabled people not carried out. These are not weaknesses of one component or another, they are gaps in the system. Lifting any one of them would move the whole catalogue up at once.

The twelve components of the catalogue as twelve rows, against the three maturity levels: Proven, Consolidated, Core. For each one the first two levels are filled and the third stays empty — twelve strictly identical rows. A continuous black rule runs the full height just before the Core level: the ceiling. To its right, the three blockers, named once because they hold for all twelve: internationalisation unverified, no visual regression tests, no testing with disabled users. Lifting any one of the three moves all twelve up at once.
The maturity grid measures the engineering rigour around a component, not its delivered quality. It is built to demote.

Results

  • Complete foundations: two-tier variables, light and dark modes, tokens exported in DTCG format to a 3.4 KB tokens.css.
  • 17 components designed, of which 12 prototyped in native HTML, CSS and JavaScript, documented and verified in the browser.
  • Zero axe-core violations across the prototyped catalogue, confirmed by keyboard, at 200% zoom and with a screen reader.
  • Two composed patterns, including a measured comparison against the GOV.UK Design System: 16× less JavaScript at equal compliance.
  • A documentation site of some twenty pages, built with the system itself and generated with no dependency whatsoever.
  • Two themes obtained by a mode switch, with no component duplicated.
  • Zero npm dependencies across the whole project, documentation site included.
  • Written governance: SemVer, changelog, roadmap, process template, maturity grid.

What the system does not do yet

Three limits, and I would rather write them down than let them be discovered.

The weight grade is an internal rank, not an absolute score. It places a component relative to the eleven others in the catalogue. The accessibility grade, by contrast, is absolute. That asymmetry is documented and accepted, but it forbids comparing a weight letter to one from another system.

The five supervision components are designed, not yet measured. They exist in the library, with their variants, states and usage descriptions. They have neither prototype nor scoring sheet, so no grade. Assigning them one by analogy would have been the opposite of what this project argues for.

No testing has been carried out with disabled people. The audit is technical: axe-core, Lighthouse, keyboard, VoiceOver. That is the known gap against GOV.UK, and it sits at the top of the roadmap.

What I learned

The accessible choice is almost always the leanest one. I expected a permanent trade-off between compliance and lightness. Twelve times out of twelve, the native element won on both counts at once. The tension does exist, but it lies elsewhere: in the self-hosted typeface, which accounts for 73% of first-visit weight and remains an accepted legibility choice.

Verifying upfront costs less than fixing downstream. The only compliance gap found during the project was the contrast of a field border, which automated checkers do not detect. It was fixed once, by creating a reusable token, and every component built afterwards inherited the fix. Found three months later in an end-of-pipeline review, it would have had to be fixed everywhere it had spread.

The accessibility of a screen cannot be delegated to the catalogue. That is the result that surprised me most. The error summary, the data freshness, the refusal of an over-talkative live region: none of it belongs to a component. A system can be flawless in detail and still let an unusable screen through.

Get in touch about a project like this