Skip to content

Theme lab

MDS 2.0 / FORGE

A design system that an AI can read

I owned the color and component-state foundations for MDS 2.0, a 1,830-variable Figma library connected to Flutter through Code Connect. I then framed FORGE, an experimental pipeline that reduces legacy screens to structured JSON for model-assisted modernization.

Year
2025—26
My scope
Owned the color foundation, component-state matrices, and button Code Connect mapping; framed the experimental modernization pipeline built on top.
Collaborators
One of three designers on the library, working with Flutter engineers who owned the code implementation.

1,830

Figma variables across nine collections

The Figma Variables editor with nine collections listed and the TextInput States matrix open: fifteen rows resolving stroke, radius, label and text roles across five component states.
The Figma Variables editor with nine collections listed and the TextInput States matrix open: fifteen rows resolving stroke, radius, label and text roles across five component states.

Three copies of the truth

A design system tends to exist three times. Once as a Figma library, once as the code that implements it, once as documentation describing both. Each copy has a different owner and a different update cadence, so they drift. Add an AI assistant to the workflow and there is a fourth copy, because the model needs an instruction set and somebody writes it by hand.

MDS 2.0 was an attempt to collapse that into one thing. A token architecture in Figma, bound to the Flutter codebase through Code Connect, with component rules written in a form a model could read directly. Not a library plus docs plus prompts. One artifact with several outputs.

I owned the color foundation, the component-state variable matrices, and the Code Connect mapping for the button set. Most of my time went to the seam between those, which is also where all of the arguments were.

A component library, its documentation, and a model's instruction set are one artifact rendered three ways. Most teams maintain three.

Two tiers, and one collection that exists for the compiler

Seven named tonal ramps at thirteen stops each. Nothing consumes a raw palette value. Semantic theme roles alias into the ramps, and every role carries an explicit light assignment and an explicit dark one, so a theme swap is a lookup rather than a redesign. Elevation is authored twice for the same reason.

Material 3 gives you primary, secondary, tertiary and error. It does not give you success, warning, danger and info as themed roles, so teams invent them per screen and the greens stop matching. We promoted all four to first-class themes with full light and dark pairs, in design and in code. That means leaving the spec, and every future Material upgrade now needs a reconciliation pass. It was still cheaper than the alternative, which was status color as folklore.

About 1,830 variables across nine collections. The split matters more than the count: primitives separated from semantics, semantics separated from component-state matrices, and one collection built purely to serve the codebase rather than the canvas. Component interaction state runs off variable modes instead of hand-built variants. A text input resolves fifteen bound properties across five modes, so the stroke walks from outline to primary to danger by state and the floating label is a boolean, with no duplicated variant holding either.

The bridge, wired

Code Connect maps one Figma button set to five Flutter symbols, including a named constructor. The mapping targets Flutter's `ThemeExtension` classes rather than bespoke wrapper widgets.

That choice has a visible cost. The snippet Dev Mode hands an engineer is a plain `FilledButton(...)`. Nothing in it names the design system, which makes the handoff demo look like nothing happened, and it means adoption cannot be audited by reading a screen. It is also correct: theming belongs in the theme, not in a parallel widget layer somebody has to keep in step with the framework.

The spec board and the sandbox are deliberately separate objects. The master variant set is locked. Next to it sits a playground of roughly sixty rendered permutations including a disabled state for every style, because the question a developer actually has is not what the spec says, it is what this looks like when it is off.

FORGE

FORGE is the pipeline built on top. Three ingest paths converge on one representation: analysis of legacy screens, the proven path from Figma files already built out of MDS components, and an R&D path starting from prompt-generated HTML. All three produce the same enriched JSON. Per-component skill files are what the model reads, and those files are treated as source of truth rather than as prompt scaffolding.

The enrichment step is what made it work. Raw Figma JSON is mostly noise for this purpose. Enriching and reducing it cut the parse payload from roughly ten thousand lines to roughly one thousand, which is the difference between a model holding context for a screen and a model holding context for a button.

Which model runs is a controlled variable in the design rather than an implementation detail, so output can be compared across models instead of argued about.

What shipped, and what remained experimental

Code Connect was wired and reporting connected. The artifact I can show is the panel rather than the `figma.connect()` source file, so that is the limit of the public evidence.

FORGE's enrichment step reduced the raw parse payload from roughly ten thousand lines to roughly one thousand. End-to-end Flutter generation was not feasible when I last worked on it, output quality had not been benchmarked, and adoption was not instrumented.

If I returned to it, I would instrument adoption first. A design system cannot improve a workflow if nobody can answer whether it is being used.

Artifacts

  • The MDS 2.0 library at 24% zoom, showing seven named tonal palettes, the light and dark theme boards that alias into them, and separate elevation effect styles per mode.
    The MDS 2.0 library at 24% zoom, showing seven named tonal palettes, the light and dark theme boards that alias into them, and separate elevation effect styles per mode.Colleagues' first names in a page header and a page title are blurred, along with an account avatar.
  • Figma's Code Connect panel reporting Connected, mapping one button component set to five Flutter theme-extension symbols including FilledButton.tonal, above the generated Dart preview.
    Figma's Code Connect panel reporting Connected, mapping one button component set to five Flutter theme-extension symbols including FilledButton.tonal, above the generated Dart preview.Colleagues' first names in the page header and a collaborator's presence avatar are blurred.
  • A FigJam board diagramming the FORGE pipeline: three ingest paths converging on one JSON-enriched representation, followed by model selection, planned Flutter output, and a quality benchmark.
    A FigJam board diagramming the FORGE pipeline: three ingest paths converging on one JSON-enriched representation, followed by model selection, planned Flutter output, and a quality benchmark.A colleague's avatar and two first names attached to specific pieces of work are blurred.

Decisions

  1. The decision

    Point Code Connect at Flutter `ThemeExtension` classes instead of building a wrapper widget for every component.

    The tradeoff

    Wrappers would have made the system visible in application code and trivial to audit. Theme extensions make the generated snippet a plain `FilledButton(...)` with nothing in it that names MDS, so the handoff looks unimpressive in a demo and adoption cannot be checked by reading a screen.

    The consequence

    No parallel widget layer to version against the framework, and engineers write ordinary Flutter that inherits the system for free. Auditing moved to the theme layer, which is a handful of files rather than hundreds of call sites.

  2. The decision

    Drive component interaction state from Figma variable modes rather than from hand-built variants.

    The tradeoff

    Modes are invisible to a designer browsing the library and harder to teach than a variant picker. A designer who wants a one-off error state has to understand the matrix or detach the component, and some detached.

    The consequence

    One text input holds fifteen bound properties across five states with nothing duplicated. Changing the error stroke is a single edit that lands everywhere, and the matrix reads to an engineer as a table of resolved values rather than as a pile of variants.

  3. The decision

    Insert a JSON enrichment step and treat per-component skill files as source of truth, instead of writing better prompts.

    The tradeoff

    One more artifact for the design team to maintain, and an artifact engineers now depend on at build time. It slowed the pipeline down and put the design system team inside a code path they had never been in before.

    The consequence

    Parse payload dropped from roughly ten thousand lines to roughly one thousand, and the prototype consumed the same component instructions on every run. That reduced prompt-by-prompt variation without claiming end-to-end output quality we had not benchmarked.

Design systemsFigma Code ConnectFlutterDesign tokensAI tooling