Petro Dudchenko

BIGVU

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
One canonical name β†’ four platform dialects
bg-action-strong
Figma
Backgrounds (Surfaces)/Primary Action/bg-action-strong
Web
var(--bg-action-strong)
iOS
BigvuColorsSemantic.bgActionStrong
Androidmigrating
@color/bg_action_strong
Reach
4
platforms, one system
40
components, one recipe
Architecture
2-layer
colour model (primitives to semantic)
1 name
per token, every platform

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.

CategoryLayersWhy
Colour2primitives to semantic. The only category that changes between light and dark.
Spacing1one flat, pixel-named scale
Radius1a small, role-named set
Typography1a role-named scale
Alpha0built 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-token adds a token in the right category and checks the name against the rules.
  • /audit-tokens flags tokens used zero or one time, so they can be removed.
  • /check-drift diffs the Figma exports against web, iOS, and Android. It fails CI when they disagree.
  • /apply-tokens applies semantic tokens to an unstyled Figma frame via the Figma MCP.
DESIGN.mdYAML Β· contract
# DESIGN.md β€” the contract between Figma, Web, iOS, Android
architecture:                  # how many layers each category actually needs
  colors:
    layers: 2
    structure: primitives β†’ semantic   # the only category that shifts light/dark
  spacing:    { layers: 1 }    # one flat pixel-named scale
  radius:     { layers: 1 }    # small role-named set
  typography: { layers: 1 }    # one role-named scale
  alpha:      { layers: 0 }    # composed inline from RGB fragments β€” not a token

platform_mapping:              # one canonical name β†’ a dialect per platform
  example_token: bg-action-strong
  figma:   Backgrounds (Surfaces)/Primary Action/bg-action-strong
  web:     var(--bg-action-strong)
  ios:     BigvuColorsSemantic.bgActionStrong
  android: "@color/bg_action_strong"

rituals:                       # the rules, runnable as commands
  - /new-token     # add a token to the right category, validate the name
  - /audit-tokens  # find tokens used 0–1 times β†’ inline or remove
  - /check-drift   # diff Figma vs Web/iOS/Android; fails CI on mismatch
  - /apply-tokens  # Figma MCP applies semantic tokens to a frame

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.scssSCSS Β· Web
// _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.jsonJSON Β· Figma export
// 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 },
  // …
}
scripts/generate-tokens.jsJavaScript Β· generator
// scripts/generate-tokens.js β€” Figma export β†’ native token files
const TOKENS_PATH    = path.join(__dirname, '../src/lib/tokens/Light.tokens.json');
const COMPONENTS_DIR = path.join(__dirname, '../src/lib/components');

function generateTokens() {
  const tokensJson = JSON.parse(fs.readFileSync(TOKENS_PATH, 'utf8'));

  for (const componentName of Object.keys(tokensJson)) {
    if (componentName.startsWith('$')) continue; // skip metadata

    const dir = COMPONENT_MAPPING[componentName] || toKebabCase(componentName);
    // …collect this component's colour tokens from the export…

    const outputPath = path.join(COMPONENTS_DIR, dir, dir + '-colors.scss');
    fs.writeFileSync(outputPath, fileContent);
  }
}
ColorsSemantic.swiftSwift Β· iOS result
// 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
    }
}
generate-tokens Β· check-driftTerminal
$ node scripts/generate-tokens.js
reading src/lib/tokens/Light.tokens.json
βœ“ button β†’ button/button-colors.scss
βœ“ input β†’ input/input-colors.scss
βœ“ modal β†’ modal/modal-colors.scss
βœ“ dropdown β†’ dropdown/dropdown-colors.scss
…
βœ“ 40 components, token files written
$ npm run check-drift
diff Figma ↔ web Β· iOS Β· Android
βœ“ web in sync
βœ“ iOS in sync
βœ— android semantic layer missing (migrating)
βœ— drift on 1 platform β†’ CI fails

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.

1 Β· Size1 / 5

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
// sizes.ts β€” dimensions from the shared scale
buttonSizes.md = {
  height: '40px',
  paddingX: '12px',
  radius: '10px',
}
2 Β· Type2 / 5

Add type from the shared role scale. Inter, 16px, weight 600. The label now reads like the rest of the product.

typography.ts
// typography.ts β€” type from the role-named scale
buttonTypography.md = {
  fontFamily: 'Inter, sans-serif',
  fontWeight: 600,
  fontSize: '16px',
  lineHeight: 1.4,
}
3 Β· Colour3 / 5

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
// button-colors.scss β€” colour by role
'primary-bg': 'bg-action-strong',  // β†’ blue-500
'primary-fg': 'fg-on-color',       // β†’ white
4 Β· States4 / 5

States 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
// 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-100
5 Β· Variants5 / 5

Finally, 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.

Style
button.ts
// 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.tsTypeScript Β· Angular
// 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.

src/lib/Structure Β· Angular
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 files
The component library

Every component reads from the shared tokens, so the set stays consistent as it grows.

ButtonButton IconInputCheckboxChipDropdownMenuMenu ItemModalAccordionAlertBadgeBannerBreadcrumbAvatarIconLinkLoaderLogo+ more

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-component Claude Code command (and a CLAUDE.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.json binds the architecture rules to every .kt file 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.

.claude/commands/new-component.mdMarkdown Β· agent command
Create a new Angular standalone component from this description:
$ARGUMENTS

## Step 1 β€” Understand the design
If a Figma URL is given, use the Figma MCP to fetch the design context
and screenshot. The Figma design is the source of truth.

## Step 2 β€” Study existing patterns
Before writing code, read 2–3 similar components in src/lib/components/
(button, input, badge…) and match the conventions you observe.

## Step 3 β€” Create the files
name.ts Β· name.html Β· name.scss Β· name.stories.ts
+ name-tokens/name-colors.scss β€” reference semantic tokens
  (bg-page-default, fg-primary), never raw hex.
firebender.jsonJSON Β· Android agent
// 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.

One token, two themes

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.

Primary
Lightbg-action-strong β†’ blue-500
Primary
Darkbg-action-strong β†’ blue-600

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.