Design System · Case Study

Nvoye Design System

Reduced handoff and code review time by 30% by building a 1000+ component design system with strict tokenization and 1:1 Figma-to-code handoff for an embassy-facing product.

9

Component families

1,135

Documented variants

4

Responsive breakpoints

1

Source of truth, Figma / React parity

Role

Senior Product Designer · theming, tokens, documentation

Product

Nvoye · Contact Management, embassy-facing

Base

MUI for Figma · Material UI v5.16.0, customised

Artefact

design_system_base · tokens, nine families, property tables

Approach

Design Principles

Before getting started with the design system, I came up with 3 key design principles to guide the approach and decision-making processes toward achieving my goals.

01

Adopt

Take MUI v5 as the substrate. Inherit nine component families, their prop names and their full state matrices instead of redrawing them.

02

Adapt

Rebrand at the token layer: palette, typeface, surfaces, radius, breakpoints. One variable change re-skins every variant downstream.

03

Document

Ship the property tables, the heading rails and the usage examples on the same canvas as the components, so the reference can't drift from the thing.

Foundations · Colour

Semantic color token naming by role

The palette is addressed as primary/main and error/error/main, not blue-500 or red. A rebrand becomes a value change inside one collection instead of a sweep through a thousand variants. Tangerine is the single accent: it earns attention precisely because nothing else in the interface competes for it.

primary/main

#3D5BA5 · brand, actions

primary/dark

#385396 · pressed

blue/900

#22325B · headings, dark surfaces

secondary/main

#F7910B · single accent

background/paper

#FDFCFB · warm base

grey/300

#F7F6F5 · card surface

error/error/main

#B50B0B · overridden ramp

warning/main

#EF6C00 · warning

info/main

#4B5A80 · info

success/main

#005B00 · success

Decision 02

Tints and neutrals are alpha fractions, not independent colours

Every text and state token is black or brand-colour at a fixed opacity. The same token then survives on white, on a #f7f6f5 card and on a tinted alert background, so nothing has to be re-specified per surface, and every new colour inherits the identical 4 / 8 / 12 / 30 / 50% ladder for free.

Ink over any surface

text/primary

87%

body copy

text/secondary

60%

labels, captions

text/disabled

38%

disabled

divider

12%

rules, borders

The one interaction ladder

…/_states/hover

4%

hover wash

…/_states/selected

8%

selected row

action/focus

12%

focus fill

…/_states/focusVisible

30%

focus ring

…/outlinedBorder

50%

outlined border

Decision 03

Warm paper, and elevation as a surface and shadow pair

Surfaces run warm (#fdfcfb, #f7f6f5, #f2f0ed) rather than neutral grey, a deliberate choice for staff who sit in dense tables for hours; pure white at this density glares. Each elevation level ships a surface token and a shadow effect together, so a raised card still reads when shadows are suppressed, in print or in high-contrast mode.

paper/elevation-0

#FFFFFF · flat

paper/elevation-1

#FBFAF8 · + elevation/1

paper/elevation-2

#F7F6F5 · + elevation/2

paper/elevation-4

#F2F0ED · + elevation/4

paper/elevation-8

#FEFDFB · + elevation/8

background/default #121212 is reserved for a dark mode that is not built yet.

Foundations · Type

Standardizing Font Styles

Material ships on Roboto. The brand runs on Outfit. Because type is bound as variables (typography/h1 … _Component/button/large), the move was a value change inside the token collection rather than a per-component edit, so the whole library re-set itself.

The Typography family: 15 variants × gutter-bottom, each one bound to a type token.

Decision 04

Embedding custom scales in the Typography component

Material's ramp jumps from body1 at 16 to h6 at 20, with nothing in between. Contact records needed a field-group label that was louder than body copy but quieter than a heading. Rather than add two loose text styles, which only exist in Figma and quietly die at handoff, subhead1 and subhead2 were added as variants of the Typography component, so they arrive in the prop enum as variant="subhead1".

Decision 05 · Accessibility

Three accessibility calls made at the token layer

01

The error ramp was overridden

Material's default error #d32f2f sits at roughly 4:1 on warm paper, so it fails small text.

02

Status colours ship as pairs

Every severity carries a background and a foreground token together .

03

Focus is a first-class state

Button, Chip, Select and Text Field each enumerate Focused alongside Hovered, drawn from the shared focusVisible token at 30%.

error #B50B0B

warning #EF6C00

info #4B5A80

success #005B00

error #B50B0B

warning #EF6C00

info #4B5A80

success #005B00

Foundations · Layout

A base-8 px grid system for horizontal and vertical spacing

Spacing

Much like a seasoned architect who meticulously plans and arranges building blocks to construct a well-designed structure, I believe a design system's foundation is anchored in its spacing and grid.

Decision 06

Responsiveness as a property

<Container> carries Max width (Desktop / Laptop / Tablet / Mobile) and a Disable Gutter boolean. Choosing a breakpoint is a picker selection, not a note in the margin, and it maps exactly onto the MUI Container props the engineers already use. A rule in a redline gets skipped; a rule in the variant picker gets used.

Max width

Desktop 1440 · Laptop 1024 · Tablet 640 · Mobile 375

Disable Gutter

boolean, removes the side padding

Nesting

supported, but almost never needed

Breakpoints

Breakpoints are stored as variables, not as frame names. That is what makes the next decision possible: the layout rule can be bound to a component property instead of written into a redline.

The same screen composed at mobile, tablet and laptop widths from one Container.

“The container centers your content horizontally. It's the most basic layout element.”

Shipped with the component.

Component architecture

Component library for engineering handoff

Decision 07

Detailed buton component with 400+ variants

A designer who needs size=small, color=error, variant=outlined, state=disabled finds it on the canvas instead of mocking it up. QA gets a reference image. Engineers never meet a combination that was never specified. The cost is scale (Chip alone is 448 variants) and that is only acceptable because the matrix is generated off one base component rather than drawn by hand.

405

Button variants · 3 variants × 3 sizes × 9 colours × 5 states

5

States enumerated: enabled, hovered, focused, disabled, loading

0

Undocumented combinations in the shipped families

The Button property table: axes on the rows and columns, every cell a real variant.

Decision 08

Documentation on the same canvas as the component

Each family is laid out identically: a heading rail stating category, name and purpose; a property panel naming every prop; and a property table whose rows and columns are the variant axes. There is no separate wiki to fall out of date, because the reference is the artefact.

Heading rail

Category, component name, one-line purpose, link to the MUI docs: the same four facts in the same place for all nine families.

Property panel

Every property spelled out with the name a developer will type, including the ones that are booleans rather than variants.

Property table

Axes on rows and columns, every intersection filled. Reading across a row is reading a state machine.

Alert: heading rail, property panel and severity × variant table, read left to right.

Contact Management Design System · a customised MUI v5 theme for Nvoye. Thank you for reading.