A design system in three tiers

114 primitives feed 97 semantic tokens, and the semantic tier feeds everything you can see. Nothing on this page owns a colour: switching theme flips one attribute on <html> and the rest follows.

The type ramp, the radii and the neutral colours were measured from the public stylesheet of a real money-transfer site, as a study. The brand colour, the dark mode, four derived colours and the display typeface substitution are my interpretation, and the product on the other two routes is fictional.

211 variables2 modes16 contrast pairs measured

Colour

Every semantic token is an alias onto a primitive, never a raw value. Where light and dark point at the same primitive, the token never enters the dark block of the CSS — which is how a brand surface stays put while the page around it turns.

surface — 12 tokens, 9 themed

Backgrounds. `fixed-dark` is the only one that never themes — it is identity, not mode.

--vw-surface-page#ffffff → #272727
--vw-surface-raised#ffffff → #404040
--vw-surface-sunken#f2f2f2 → #171717
--vw-surface-inverse#000000 → #ffffff
--vw-surface-brand#ffdd00 (fixed)
--vw-surface-disabled#e6e6e6 → #5a5a5a
--vw-surface-fixed-dark#000000 (fixed)
--vw-surface-brand-veil#ffffffb3 → #000000b8
--vw-surface-media-veil#f2f2f2 → #000000
--vw-surface-inset#f2f2f2 → #212121
--vw-surface-brand-bar#eacb00 (fixed)
--vw-surface-inset-on-brand#f2f2f2 → #5a5a5a

text — 11 tokens, 6 themed

Text colours, including the ones that live on surfaces which never invert.

--vw-text-primary#000000 → #ffffff
--vw-text-secondary#5a5a5a → #bfbfbf
--vw-text-disabled#999999 → #5a5a5a
--vw-text-inverse#ffffff → #000000
--vw-text-inverse-secondary#999999 → #5a5a5a
--vw-text-link#0859a6 → #4da6f7
--vw-text-on-brand#000000 (fixed)
--vw-text-on-fixed-dark#ffffff (fixed)
--vw-text-on-fixed-dark-secondary#999999 (fixed)
--vw-text-on-media#ffffff (fixed)
--vw-text-on-media-secondary#d9d9d9 (fixed)

border — 7 tokens, 5 themed

`default` clears 3:1 in both modes; `subtle` is decorative and does not have to.

--vw-border-subtle#e6e6e6 → #5a5a5a
--vw-border-default#767676 → #999999
--vw-border-strong#000000 → #ffffff
--vw-border-focus#0a8cf2 → #4da6f7
--vw-border-inverse#404040 → #bfbfbf
--vw-border-on-fixed-dark#404040 (fixed)
--vw-border-on-brand#121212 (fixed)

action — 18 tokens, 9 themed

Each button type carries four background states and two foregrounds.

--vw-action-primary-bg#ffdd00 (fixed)
--vw-action-primary-bg-hover#ffc300 (fixed)
--vw-action-primary-bg-active#fff1be (fixed)
--vw-action-primary-bg-disabled#e6e6e6 → #5a5a5a
--vw-action-primary-fg#000000 (fixed)
--vw-action-primary-fg-disabled#999999 (fixed)
--vw-action-secondary-bg#000000 → #ffffff
--vw-action-secondary-bg-hover#272727 → #f2f2f2
--vw-action-secondary-bg-active#404040 → #e6e6e6
--vw-action-secondary-bg-disabled#e6e6e6 → #5a5a5a
--vw-action-secondary-fg#ffffff → #000000
--vw-action-secondary-fg-disabled#999999 (fixed)
--vw-action-tertiary-bg#00000000 (fixed)
--vw-action-tertiary-bg-hover#f2f2f2 → #404040
--vw-action-tertiary-bg-active#e6e6e6 → #5a5a5a
--vw-action-tertiary-bg-disabled#00000000 (fixed)
--vw-action-tertiary-fg#000000 → #ffffff
--vw-action-tertiary-fg-disabled#999999 (fixed)

feedback — 12 tokens, 10 themed

bg and fg always come from one family — that is what holds contrast when the mode flips.

--vw-feedback-info-bg#d0e1ee → #0859a6
--vw-feedback-info-fg#0859a6 → #d0e1ee
--vw-feedback-success-bg#c9f1e8 → #025240
--vw-feedback-success-fg#025240 → #c9f1e8
--vw-feedback-error-bg#f6ddd5 → #5a1502
--vw-feedback-error-fg#a52603 → #f6ddd5
--vw-feedback-error-surface#ffe2e2 → #8e3737
--vw-feedback-error-inset#ffb7b7 → #710f0f
--vw-feedback-error-text#cd0000 → #ffffff
--vw-feedback-error-text-secondary#a52603 → #ed967b
--vw-feedback-warning-bg#7a5c00 (fixed)
--vw-feedback-warning-fg#fff1be (fixed)

Typography

Eleven styles, each bound to a role (type/body, type/page-title) which in turn points at the raw ramp. Move the role and everything using it moves; move the ramp and every role moves.

Display/H3Archivo · weight 600type/page-title3xl / 4xl
Send money across the world
Display/H4Archivo · weight 600type/section-title2xl / 3xl
Send money across the world
Display/H5Archivo · weight 600type/subsection-titlexl / 2xl
Send money across the world
Display/H6Archivo · weight 600type/card-titlelg / lg
Send money across the world
Title/04Roboto · weight 500type/leadlg / xl
Send money across the world
Label/01Archivo · weight 500type/action-labelmd / lg
Send money across the world
Label/02Archivo · weight 500type/overlinexs / xs
Send money across the world
Body/01Roboto · weight 400type/bodymd / md
Send money across the world
Body/02Roboto · weight 400type/body-compactsm / sm
Send money across the world
Body/03Roboto · weight 400type/captionxs / xs
Send money across the world
Display/H2Archivo · weight 600type/hero-title4xl / 5xl
Send money across the world

Space, radius and elevation

space/none · 0
space/xs · 4
space/sm · 8
space/md · 12
space/lg · 16
space/xl · 24
space/2xl · 32
space/3xl · 40
space/4xl · 48
space/5xl · 64
space/2xs · 2
space/xs-plus · 6
radius/none · 0
radius/xs · 4
radius/sm · 8
radius/md · 12
radius/lg · 16
radius/pill · 48
radius/full · 999
radius/xs-plus · 6

Elevation/sm

Shadows carry more weight in dark, where the ground is already black.

Elevation/md

Shadows carry more weight in dark, where the ground is already black.

Elevation/lg

Shadows carry more weight in dark, where the ground is already black.

Icons

Sixteen drawings at 24×24, all painted with currentColor — an icon never disagrees with the text beside it.

arrow-leftarrow-left
closeclose
chevron-downchevron-down
checkcheck
more-horizontalmore-horizontal
cashcash
bankbank
walletwallet
clockclock
backspacebackspace
alertalert
shieldshield
sunsun
moonmoon
cardcard
downloaddownload

Components

These are the same instances the marketing page and the app prototype render. There is no separate mobile set.

Button — 3 types × 2 sizes × icon slot

Input — states

Fee shown before you confirm

Over the $920.00 daily limit.

Corridor closed

Badge, list and timeline

ProcessingDeliveredRejectedReady to collect
Maria Silva — Cash pickupSão Paulo, Brazil · 8472 1193 04$250.00Delivered
John Pereira — Bank depositLisbon, Portugal · 5590 2284 71$480.00Processing
Transfer submittedPayment authorised
Funds convertedRate locked at submission
Ready to collectPending
Tracking number5590 2284 71
Fee$4.08

Contrast

Computed at build time from these same tokens, not copied out of a spreadsheet. Text needs 4.5:1, non-text needs 3:1. 15 of 15 pairs clear their minimum in both modes. Disabled text is listed too: it is exempt, and the table says so rather than leaving it out.

PairMinimumLightDark
Body copy
text.primary on surface.page
4.521 ✓14.94 ✓
Secondary text
text.secondary on surface.page
4.56.9 ✓8.12 ✓
Link
text.link on surface.page
4.57.02 ✓5.77 ✓
Text on brand
text.on-brand on surface.brand
4.515.59 ✓15.59 ✓
Primary button
action.primary.fg on action.primary.bg
4.515.59 ✓15.59 ✓
Secondary button
action.secondary.fg on action.secondary.bg
4.521 ✓21 ✓
Badge · info
feedback.info.fg on feedback.info.bg
4.55.25 ✓5.25 ✓
Badge · success
feedback.success.fg on feedback.success.bg
4.57.55 ✓7.55 ✓
Badge · error
feedback.error.fg on feedback.error.bg
4.55.64 ✓10.51 ✓
Badge · warning
feedback.warning.fg on feedback.warning.bg
4.55.53 ✓5.53 ✓
Footer heading
text.on-fixed-dark on surface.fixed-dark
4.521 ✓21 ✓
Footer link
text.on-fixed-dark-secondary on surface.fixed-dark
4.57.37 ✓7.37 ✓
Control border
border.default on surface.page
34.54 ✓5.24 ✓
Focus ring
border.focus on surface.raised
33.47 ✓4.01 ✓
Outline on brand
border.on-brand on surface.brand-bar
311.63 ✓11.63 ✓
Disabled control text
text.disabled on surface.page · exempt under WCAG 1.4.3, disabled fields only
4.52.85 exempt2.17 exempt

Motion

Durations and curves are variables too — TIMING and EASING types, which almost nobody uses. The app prototype drives its screen transitions from these.

instant

100ms

fast

150ms

base

250ms

slow

400ms

enter

450ms

toggle

200ms

gentle

800ms

motion/ease/standard — cubic-bezier(0.2, 0, 0, 1)

motion/ease/enter — cubic-bezier(0, 0, 0, 1)

motion/ease/exit — cubic-bezier(0.3, 0, 1, 1)

motion/ease/gentle — cubic-bezier(0.22, 1, 0.36, 1)