Chongbo NingDesign Engineer, London

Logistics infrastructure company

One Design System,
Three Products,
Any Brand

Started in Figma, shipped as code, and layered so anyone can restyle it without breaking contrast.

Seen as
Role
Design Engineer
  • Figma library, colour scales and code on my own
Outcome
One design system across the company's products, which every client and user can customise on top.

What this shows

  • Design system
  • Figma to code
  • Accessibility in the colours

Part 1: One system for every product and client

Drifting apart · Custom branding · A four-level solution

01.1Problem

Three products from one company, drifting apart

The company began with one product and its own colours and design tokens. New products took only those first tokens with them, and each needed things the first didn't have.

Built fast by different teams, much of it with AI coding tools, each product changed or dropped what didn't fit. With every release they looked less alike.

01.2Need

Every client wants its own look

Even within one product, our customers are companies with brands of their own, and each wants it to feel like theirs: their colour, their typeface, the shape of their buttons.

01.3Solution

Each level overrides only what it needs

I split the system into levels. The shared package is the foundation: it sets every colour, role and component and runs through everything, so a fix there reaches all three products. What only one product needs sits in the second level, built from existing tokens rather than new colours.

Within one product, each client sets its own colour, font and button shape for all its users, and each user can adjust their own look again as they need.

Courier4Userstheir own look3Clientscolour, font, button shape2Productsits own parts, hatched1Shared packagesets every token

Part 2: From Figma to code

A Figma library · Kept in code

02.1

It started as a Figma library

I built the system in Figma first, from the main product's shadcn components: colour variables with Light and Dark modes, and components with their variants and states.

Design system · rebuilt on an invented brand
Components
  • Button12
  • Text input4
  • Badge5
  • Alert4
  • Pagination item3
Variants and states, with every colour bound to a variable.
Review · Light
The Figma library's components in the light mode: buttons, inputs with focus and error, badges, alerts and pagination.
Variables
  • role/primary/fill#3453D3
  • role/primary/text#3453D3
  • role/success/text#00782D
  • role/warning/text#7E5400
  • role/destructive/text#BB061E
Collections

Colour · Light
Colour · Dark
Radius

02.2

Now it lives in code

In the end I shipped it as a code package, so teams build with its parts instead of redrawing them from a design file.

With AI coding tools I built one shared tokens file that sets every colour and role. Each product imports it, and its own globals.css overrides only what that product's interface needs.

design-system
/* shared package: the foundation */:root {  --brand-50:  oklch(0.97 0.014 268);  --brand-200: oklch(0.82 0.089 268);  --brand-600: oklch(0.5 0.2 268);  --brand-700: oklch(0.44 0.17 268);   /* roles: for components */  --primary: var(--brand-600);  --primary-foreground: oklch(0.985 0 0);  --ring: var(--brand-200);  --success-text: var(--green-600);  --warning-text: var(--amber-700);} .dark {  /* text steps re-tuned, not inverted */  --brand-600: oklch(0.76 0.122 268);  --primary: oklch(0.5 0.2 268);}
localhost:3000/preview
SettingsSettingsSettingsSettings
Placeholder text
DeliveredDelayedFailedOn the roadToday

Part 3: Accessibility that holds in any colours

Roles, not colours · One lightness per step · Dark mode · Customer colours · Try it · Outcome

03.1

Components ask for a role, not a colour

Every hue has five steps, each with one job: surfaces, borders, charts and icons, text on white, and text on a tint. Components ask for success or warning, never a hue and a step, so a hue can change without touching a component.

Role
Surface50
Border200
Charts, icons500
Text on white600
Text on a tint700
primarybrand
brand 50
brand 200
brand 500
on white6.3
on its 507.5
infoblue
blue 50
blue 200
blue 500
on white6.1
on its 507.2
successgreen
green 50
green 200
green 500
on white5.6
on its 506.8
destructivered
red 50
red 200
red 500
on white6.7
on its 507.8
categoricalpurple
purple 50
purple 200
purple 500
on white6.7
on its 507.7
warningamber
amber 50
amber 200
amber 500
on white5.2
on its 506.1

03.2

The same lightness on every hue

I set the steps in OKLCH, where lightness is a number of its own, close to how bright a colour looks. The text steps sit within a few hundredths of each other on all six hues, so a role stays readable whichever hue it uses.

oklch(0.50 0.20 268)Brand text on white 6.3:1
oklch(0.50 0.145 148)Green text on white 5.6:1
L lightness, the sameC chromaH hue
Surface50brand 50: L 0.97blue 50: L 0.97green 50: L 0.97red 50: L 0.97purple 50: L 0.97amber 50: L 0.97Border200brand 200: L 0.82blue 200: L 0.82green 200: L 0.82red 200: L 0.82purple 200: L 0.82amber 200: L 0.88Charts, icons500blue 500: L 0.623green 500: L 0.64red 500: L 0.65purple 500: L 0.63amber 500: L 0.76Text on white600brand 600: L 0.5blue 600: L 0.5green 600: L 0.5red 600: L 0.52purple 600: L 0.5amber 600: L 0.54Text on a tint700brand 700: L 0.44blue 700: L 0.44green 700: L 0.44red 700: L 0.45purple 700: L 0.44amber 700: L 0.480.40.50.60.70.80.91.0OKLCH lightness
  • 30/30text pairings pass WCAG AA
  • 5.16:1lowest: amber text on white
  • 2modes: light and dark

03.3

Dark mode designed step by step

In dark mode I lowered the borders' chroma so they don't glow, and lightened the text steps to hold their contrast.

Tinted surfaces turn deep, so text on a tint still sits on something darker than itself.

Lightborder · text · text on a tint
BrandborderAa 6.3Aa 7.5
BlueborderAa 6.1Aa 7.2
GreenborderAa 5.6Aa 6.8
RedborderAa 6.7Aa 7.8
PurpleborderAa 6.7Aa 7.7
AmberborderAa 5.2Aa 6.1
Light · Dark

03.4

Readable with any customer's colour

If customers can pick any colour, a pale yellow, say, can easily put white text on its button at 1.07:1, and navy text on the dark background reads at 2.99:1.

I applied the scales' own rule to the customer's colour. The fill stays theirs, and text on it turns dark or light. Used as text, their colour keeps its hue and moves in lightness until it reaches 4.5:1.

Light
New runAa17.45:1✓
Text in the brand colourAa4.64:1✓
TodayAa4.64:1✓
Dark
New runAa17.45:1✓
Text in the brand colourAa15.54:1✓
TodayAa15.90:1✓

Try it: the system, rebuilt with invented data

The real products are confidential, so this is a rebuild. Switch the brand, the light and dark modes and the contrast labels.

Deliveries

Today's runs

ExportNew runAa12.54:1✓
Search runs
All depots
  • Run 4821DeliveredAa6.81:1✓14 stops · North depot
  • Run 4822Delayed9 stops · Harbour depot
  • Run 4823On the road11 stops · North depot
  • Run 4824Failed3 stops · Harbour depot
123…10

Run 4822

Two stops are behind schedule.Aa4.83:1✓

Delayed by 25 minutes at Harbour depot.
Note for the driver *Shown on their phone at the next stop.Write a noteA note is required.
CancelSend

03.5

Outcome

I designed the system in Figma on my own and shipped it as code for different teams to use.

Consistency
Three products built from one set of role definitions, base colours and components, each keeping its own character.
Delivery
Teams build new screens from shared code instead of redrawing them.
Accessibility
Every role passes AA in light and dark, under any customer's brand.