BIGVU runs on one design system across four platforms. Figma, web (Angular), iOS (SwiftUI), and Android. I owned it as the single source of truth. That meant the token architecture, the component APIs, and the code that keeps the platforms in sync.
This was a code job, not a Figma library. I worked in the platform repos. I defined tokens, shaped component APIs, and wrote component code so design and product don't drift apart.
- Role
- Design Systems Lead
- Scope
- Tokens, components & code
- Platforms
- Figma Β· Web Β· iOS Β· Android
- Rolled out to
- 4 designers
bg-action-strong- Figma
- Backgrounds (Surfaces)/Primary Action/bg-action-strong
- Web
- var(--bg-action-strong)
- iOS
- BigvuColorsSemantic.bgActionStrong
- Androidmigrating
- @color/bg_action_strong
Before: no system, four platforms drifting
There was no design system at all. Every new screen was built from scratch, so features were slow to ship and the UI was inconsistent. Across four platforms the problems piled up.
- Design and code had drifted. Nobody could say what the source of truth was.
- Each platform team named the same token differently.
- New components were rebuilt by hand, a little different every time.
- Light and dark, and parity across platforms, were hard to check.
The goal was to turn that into a kit of parts, so designers compose screens from building blocks and spend their time inventing instead of rebuilding.
A token system, not just a page of styles
The core idea was to match the structure to each kind of token. Each token type gets the number of layers it actually needs. There is no rigid three-layer model forced onto everything.
| Category | Layers | Why |
|---|---|---|
| Colour | 2 | primitives to semantic. The only category that changes between light and dark. |
| Spacing | 1 | one flat, pixel-named scale |
| Radius | 1 | a small, role-named set |
| Typography | 1 | a role-named scale |
| Alpha | 0 | built inline from RGB parts, so it is not a token |
None of this lives only in people's heads. DESIGN.md is the one contract all four platforms follow. It sets out the token schema, how a token maps onto each platform, and the bad habits that keep the set from growing out of control.
And it is more than a document. The rules run as commands.
/new-tokenadds a token in the right category and checks the name against the rules./audit-tokensflags tokens used zero or one time, so they can be removed./check-driftdiffs the Figma exports against web, iOS, and Android. It fails CI when they disagree./apply-tokensapplies semantic tokens to an unstyled Figma frame via the Figma MCP.
Colours get a semantic layer because the same role maps to different values in light and dark. Spacing, radius, and type stay flat. Alpha is not a token at all. Every semantic token names a role, never a value. It is bg-action-strong, not blue-500. Components reference that layer directly, so there is no third per-component layer to maintain.
// _colors-semantic.scss β semantic names, never raw values
$semantic-light: (
'bg-action-strong': color('blue-500'),
'bg-action-strong-hover': color('blue-300'),
'bg-action-strong-active': color('blue-400'),
'bg-action-subtle': color('blue-50'),
'bg-action-critical': color('red-500'),
'border-default': color('gray-200'),
// β¦
);One source, four platforms
The Figma variables are the source of truth, exported as .tokens.json. A generator reads that export and writes each platform's native file. CSS custom properties on the web, a typed enum on iOS, XML resources on Android. Nobody hand-copies values.
// Spacing/Default.tokens.json β exported from Figma variables
{
"space-0": { "$type": "number", "$value": 0 },
"space-1": { "$type": "number", "$value": 4 },
"space-2": { "$type": "number", "$value": 8 },
// β¦
}// ColorsSemantic.swift β the iOS result, light/dark built in
public enum BigvuColorsSemantic {
public static var bgActionStrong: Color {
Color.adaptive(
light: BigvuColors.blue500,
dark: BigvuColors.blue600
)
}
public static var bgActionStrongHover: Color {
BigvuColors.blue300
}
}Anatomy of one component
Every component is built to the same recipe. Watch the button come together, one token category at a time, with the reason for each.
Start with dimensions. They come from the size scale, not magic numbers. The md button is 40px tall, with 12px padding and a 10px radius. Every control on the same scale lines up.
// sizes.ts β dimensions from the shared scale
buttonSizes.md = {
height: '40px',
paddingX: '12px',
radius: '10px',
}Add type from the shared role scale. Inter, 16px, weight 600. The label now reads like the rest of the product.
// typography.ts β type from the role-named scale
buttonTypography.md = {
fontFamily: 'Inter, sans-serif',
fontWeight: 600,
fontSize: '16px',
lineHeight: 1.4,
}Colour by role, not value. background β bg-action-strong, text β fg-on-color. Naming the role, instead of βblue-500β, is what lets one token resolve correctly in light and dark.
// button-colors.scss β colour by role
'primary-bg': 'bg-action-strong', // β blue-500
'primary-fg': 'fg-on-color', // β whiteStates are part of the contract, and they run on tokens too. Hover and active step along the blue ramp. Disabled swaps to the neutral surface. Hover the button.
// button-colors.scss β states, also tokens
'primary-bg-hover': 'bg-action-strong-hover', // blue-300
'primary-bg-active': 'bg-action-strong-active', // blue-400
'bg-disabled': 'bg-disabled', // gray-100Finally, variants. One component, four roles. Each one maps to its own semantic tokens. The structure and states stay the same, and only the colour roles change. Switch styles above.
// button.ts β one component, four roles
export type ButtonStyle =
'primary' | 'secondary' | 'tertiary' | 'ghost';Live previews re-created in React from the real BIGVU tokens. The production component is the Angular one below.
That is the whole component. A typed API, every dimension and colour from a token, states and variants baked in. Assembled in code, it is just this.
// button.ts β typed API, styling driven by tokens
export type ButtonSize = 'xl' | 'lg' | 'md' | 'sm' | 'xs';
export type ButtonStyle = 'primary' | 'secondary' | 'tertiary' | 'ghost';
export class Button {
@Input() size: ButtonSize = 'md';
@Input() variant: ButtonStyle = 'primary';
@Input() disabled = false;
@Input() loading = false;
@HostBinding('style')
get hostStyles(): Record<string, string> {
const sizeToken = buttonSizes[this.size];
return {
'--btn-height': sizeToken.height,
'--btn-padding-x': sizeToken.paddingX,
'--btn-radius': sizeToken.radius,
'--btn-icon-size': sizeToken.iconSize,
};
}
}And every one of the 40 components follows that same recipe. Same structure, four platforms.
bigvu-ui-angular/
src/lib/
tokens/
Light.tokens.json // Figma export β source of truth
_colors.scss // primitives
_colors-semantic.scss // semantic (light + dark)
sizes.ts typography.ts
components/ // 40 components
button/ input/ modal/ dropdown/ badge/ β¦
scripts/
generate-tokens.js // export β native token filesEvery component reads from the shared tokens, so the set stays consistent as it grows.
Built to be extended, by people and agents
Forty components across four platforms only stay consistent if the rules are enforced, not just remembered. So I gave each repo rules the tools can read and check on their own, and an AI agent can extend the system without breaking it.
- Web. A
/new-componentClaude Code command (and aCLAUDE.md) to scaffold a component from a Figma URL, pulling the design via the Figma MCP and matching the existing token conventions. - iOS. Cursor rules (
SwiftUI_Rules.md) pin MVVM and Clean Architecture so generated views fit the codebase. - Android. A
firebender.jsonbinds the architecture rules to every.ktfile the agent touches.
The payoff is that a new component scaffolds in minutes, already on-spec. Typed API, tokens wired, states in place, instead of a hand-built one-off that drifts.
// firebender.json β AI agent rules for the Android repo
{
"rules": [
"Write clear, concise code comments",
{
"filePathMatches": "**/*.kt",
"rulesPaths": "firebender_docs/firebenderArchitectureRules.md"
}
]
}Honest about the edges
Not everything was finished, and the case should not pretend it was. Android still runs a flat legacy colour list with no semantic layer. The plan, written up in the project DESIGN.md, is to regenerate light (values/) and dark (values-night/) from the same Figma exports the other platforms already use.
And tokens have to earn their place.
- A token exists only if it is used in more than one place.
- No synonyms, no -2 suffixes, no per-component aliases that just forward a semantic token.
- No token ships without a consumer. Unused tokens rot.
The code
The system spans four repos. The Figma variable exports plus the Angular, iOS, and Android libraries. The Angular library ships a Storybook, which is the fastest way to see it running as real, interactive components.
Colour is the only category with a semantic layer, so one token resolves to a different primitive per theme. The same Button code, no overrides. bg-action-strong points at blue-500 in light and blue-600 in dark.
What it adds up to
One design language, 40 components, four platforms kept in sync from a single source. This is the system I owned end to end. Beyond the architecture, it did a few things.
- Less guesswork at handoff. Designers and engineers point at the same token.
- Gave every platform one shared set of names instead of four.
- Made token drift visible, and blocked it, in CI.
- Made building a new component repeatable instead of a one-off.
And it stuck with the team. I rolled the system's conventions out to the four designers, so they build screens from the kit, and it keeps running without me.