Building a design system with the agent that would have to use it
FigmaStyle DictionaryReactCSS ModulesRadix UITanStack TableTurborepoMCPClaude Code19 min read
A four-tier token architecture across two brands and two reading directions, thirty-two React components composed over headless primitives, and the MCP server and skills that came out of building it with an agent in the first place.
Written by
Ahmed Amr
Role
Product Designer and Design Engineer
Year
2026
In this article15
Loading case study
Tagaddod collects used cooking oil from households and restaurants and turns it
into exported feedstock. Two designers supported the whole software landscape
behind that: a customer app, an application for collectors in the field, internal
tools for orders and issue handling, operational dashboards, and products for two
brands, in English and in Arabic.
The individual Figma files were not the problem. The gap appeared when those files
became software. Spacing shifted, components behaved differently from one product
to the next, and a screen that looked consistent in design became something else
in production.
I designed the Figma library as part of my role, then built the thing it
implied but nobody had asked for: a coded system of 505 design tokens and 32
React components spanning two brands, two locales and two reading directions.
I built that library working alongside a coding agent, which meant writing
every component as an explicit specification before it existed. Doing it that
way showed me exactly which decisions a system has to state out loud, so I
encoded the rest as an MCP server and a set of skills and gave the same
grounding to everyone else's agent.
Role
Product Designer and Design Engineer
Duration
Figma library as assigned work, coded system as a self-directed project alongside it
Scope
Figma foundations and components, Token architecture, React component implementation, Documentation and enablement, MCP server and agent skills
Team
Self-directed. 148 commits, 145 of them mine.
Rendered from the live library rather than redrawn. The swatches are the semantic tokens, the specimens are the shipped Button and Badge, and the trips screen is the same Navigation, ContentPanel and Table any product imports. Only the terminal is drawn, and it borrows the system's tokens without pretending to be one of its components.
The part that was the job, and the part that was not
Standardising the Figma library was the assigned work, and it was only half an
answer. If engineers still had to inspect every frame, infer which values were
intentional, and rebuild every component locally, the source of truth stayed
visual and the drift returned the moment a product moved into code.
So I built the other half around the job rather than as part of it, with no
mandate, no headcount and no roadmap slot. The repository records 148 commits,
145 of them mine, in bursts with a six-month gap in the middle, which is what a
side project's history honestly looks like. That shaped what the system could be:
with one contributor and no review process, anything depending on discipline to
stay correct would eventually stop being correct. The architecture had to make the
right thing the default.
A visual source of truth
Engineers translated values and behaviour from screens. Each project could introduce another interpretation of the same component.
An executable source of truth
Tokens and React components carried the decisions into the product. The same implementation could be reused, tested, and improved centrally.
The goal was not to make interfaces look alike. It was to reduce how many design decisions had to be remade during implementation.
Designing it in Figma first
The Figma work set the vocabulary everything downstream inherited. Variable
collections carried the brand, locale and direction modes, and component variant
properties were structured so that they could survive being translated into a
prop API rather than staying a picker in a panel.
That last constraint did most of the work. A variant property that means one
thing to a designer and nothing to a developer is a decision that gets remade at
implementation time. So the properties were named and grouped as if they were
already the component's public interface, and the React version kept the shape.
There is no automated pipeline here, and I would rather say so than imply one.
No Code Connect, no variable export, no generated token file. The Figma
collections and the token JSON were held in agreement by naming discipline and
by the fact that one person owned both. That is a real limitation of the setup
and it is the first thing I would replace with a second contributor.
The token architecture
The system resolves four tiers into one value.
Core holds 152 raw primitives, including 96 colours as six families of
sixteen steps, plus size, space, radius and border-width scales. Semantic
holds 117 roles that alias those primitives and give them meaning: fill, surface,
border, text, icon, overlay. Extras holds 85 more for shadow, motion and
z-index. On top sit the overlays, 151 tokens where brand, locale and
direction override only the parts that genuinely change.
505 definitions across 21 files, authored in the W3C design-token format so the
$type and $description fields carry meaning rather than convention. Style
Dictionary compiles them through a custom-registered format that emits a --t-
prefix under a configurable selector, and it runs seven separate builds: one
base, one per brand, one per locale, one per direction. 579 CSS custom properties
come out the other side.
The package then exports each axis separately, so a consumer imports
./brands/greenpan.css or ./locales/ar.css and takes on only the axes it
actually needs.
Nothing is overridden. Every value on the card resolves through the defaults.
Core
:root
blue
1200 #3959fe
green
gray
1600 #16161d
space
100200300400600800
radius
100200300400
Semantic
:root
fill.brand→ gray.1600Main actions, like primary buttons.
fill.brand-secondary→ gray.500Brand elements with a secondary level of prominence.
text.link→ gray.1600Text links.
text.primary→ gray.1600Body text and headings.
text.secondary→ gray.1200Descriptions or helper text.
fill.success-secondary→ green.200Success, on a smaller surface, with secondary prominence.
border.secondary→ gray.700Differentiates a section or card placed on a surface.
Brand-neutral by design. fill.brand is near-black at this tier and only becomes a colour one tier down.
Brand
[data-theme="tagaddod"]
Tagaddodrestates 19 colour tokens
fill.brand→blue.1200
text.link→blue.1200
fill.brand-secondary→blue.400
GreenPanoverrides 4 colour tokensand moves the core primitive green.1200 itself, from #007d44 to #009f4d
fill.brand→green.1200
text.linknot overridden
fill.brand-secondarynot overridden
Locale
[lang="en"]
Outfit
Trip400Trip500Trip600Trip700
Tajawal
رحلة500رحلة700رحلة700رحلة700
One token, font.weight.regular, resolves to 400 in English and 500 in Arabic. Tajawal reads lighter at the same nominal weight, and its ladder has fewer usable steps, so the top three collapse to 700.
Direction
[dir="ltr"]
ltr
startpadding-inline-start: 1rem·text-align: start
rtl
startpadding-inline-start: 1rem·text-align: start
No value changes between the two rows. The properties are logical, so only the side they resolve to moves.
Eight combinations from one composition. Brand reaches the semantic tier and, in GreenPan’s case, one core primitive; locale swaps the family and the whole weight ladder; direction changes no value at all. Everything on the card is one of the tokens above, resolved.
The detail that mattered most turned out to be the least technical one. Every
semantic token carries a written description of when to use it, not just what it
resolves to. color.border.tertiary says it is "rarely used, to make a less
emphasized section border." I wrote those for the humans reading the docs site.
They became the reason an agent could later pick the right token instead of the
closest one, which is a thing I did not plan and would not have predicted.
No component-token tier
Chose
Core and semantic roles, with brand, locale and direction as overrides resolved at runtime
Trade-off
A component-token layer would allow per-component theming without touching semantics, and build-time theme generation would produce smaller stylesheets than runtime custom properties.
Both alternatives buy granularity at the cost of a naming surface that one
person has to maintain and document. Runtime custom properties also make the
three axes genuinely independent: brand, locale and direction compose without
a combinatorial build, which is what lets the same component render in eight
configurations from one implementation.
Arabic was a typographic problem, not a mirroring one
Supporting Arabic is where most systems stop at dir="rtl" and call it done.
The parts that actually break are quieter than that.
English and Arabic do not share a weight ladder. Outfit and Tajawal are paired
families, but Tajawal reads lighter at the same nominal weight, so a token called
font.weight.regular cannot resolve to 400 in both. In this system it resolves
to 400 in English and 500 in Arabic, and the ladder above it compresses:
English runs 400, 500, 600, 700 while Arabic runs 500, 700, 700, 700. Arabic has
fewer usable steps, so the system stops pretending otherwise.
Line height is the second one. Arabic ascenders and descenders occupy more of the
box, so type set to a comfortable English rhythm either clips or floats. The
components trim the line box with zero-height pseudo-elements and pull it back by
a different amount per direction.
The line-height trim, from the implementation guideCLAUDE.md
.textElement::before { margin-top: -0.15em; }.textElement::after { margin-bottom: -0.15em; }/* Arabic sits taller in the box, so the trim is larger */:global([dir="rtl"]) .textElement::before { margin-top: -0.25em; }:global([dir="rtl"]) .textElement::after { margin-bottom: -0.25em; }
The third is a one-line decision with a disproportionate effect. Locale tokens
are scoped to [lang="ar"] rather than html[lang="ar"]. Anchoring them to the
document element means Arabic can only ever be the whole page. Scoping them to
any element means a single card, a preview pane, or one column of a comparison
can render in Arabic inside an otherwise English document, which is what makes
the documentation site able to show both at once.
Underneath that sits the ordinary work: 270 [dir="rtl"] rules across 27
stylesheets, 83 uses of logical inline properties, and directional icons that
express meaning rather than a fixed physical side. A component was not finished
because it remained usable after the direction flipped. Its hierarchy,
alignment, icon direction and text rhythm had to stay deliberate.
One component, four contexts, no fork. Brand changes a single semantic token; direction changes the side every element starts from. The Arabic row is genuinely shorter than the English one, because Tajawal carries a tighter line height and a heavier weight ladder. That is a locale token rather than a visual correction.
Building the components with the agent that would use them
I did not write the component library by hand and then document it. I wrote the
specification for each component first, in enough detail that a coding agent
could build it, and used the Figma MCP to feed the design context in alongside.
Which headless primitive it composed over, which props were public and what they
were called, which tokens applied where, what the RTL treatment had to be.
This was a practical decision before it was an interesting one. One person
cannot hand-write 32 component families with tests and documentation alongside a
full design role. But it had a consequence I did not anticipate: every place
the agent produced something wrong was a place the system had not stated a
decision out loud. A vague spec produced a plausible component with an invented
prop name. A precise one produced something I could review in a minute.
Debugging the agent's output was really debugging the specification. That is
what the MCP and the skills later turned into.
The library composes over solved problems rather than rebuilding them. Twelve
Radix packages provide accessible interaction primitives, declared as peer
dependencies rather than vendored so a consuming app resolves a single copy of
each. TanStack Table handles table behaviour, dnd-kit drag and drop, Vaul
drawers, Sonner toasts, React Day Picker calendar logic. My work is the layer around them: the
API, the tokens, the direction handling, and the parts that only break when
components meet each other.
The z-index system is the clearest example of that last category. A commit
titled "fix overlaping z-index issue between all components" is where a popover
opening from inside a modal, and a select living inside a drawer, stopped being
individually patched and became nine z-index tokens with explicit values for the
nested cases: modal-dropdown at 1060, drawer-modal at 1070. A bug that
recurred across components became a decision made once.
Every control here is the shipped component at its real size, arranged with the prop sets its own preview file declares. The family resemblance across three unrelated groups is the point: accessible behaviour comes from Radix, TanStack, dnd-kit, Vaul, Sonner and React Day Picker, while the API, the tokens and the right-to-left treatment are defined in the library itself.
An API shaped so it could describe itself
Variants are string unions with the option list exported alongside the component
as a frozen array. Button ships buttonVariants, buttonTones and
buttonSizes next to the component itself.
That one choice paid for itself three times. The documentation site's controls
panel reads those arrays to build itself. The previews enumerate them to render
every state without a hand-maintained list. And when the MCP server arrived, it
did not need a manifest of what props exist, because reading the source answers
the question. The component describes itself in the only place that cannot drift
from the implementation, which is the implementation.
Compound components appear where the primitive earns them: Tabs with its list,
triggers and content; Popover with root, trigger, content, arrow and close;
Table with column factories for the cell types the products actually use.
The token discipline is measurable. Across the library's stylesheets there are
2,321 token references resolving to 314 distinct tokens. There are also 20
hardcoded hex values, and being precise about them matters: every one is a
fallback inside var(), never a bare literal. Several of those fallbacks still
name Tagaddod blue, which means that if a token ever failed to resolve, the
GreenPan theme would quietly render a blue that no theme could override. That is
the same family of bug as the ones further down this page, and it is the cost of
a system with one reviewer.
Then everyone else's agent needed the same context
Product managers and product engineers were starting to build working tools with
AI assistants. An agent could generate a page quickly, but it did not know our
component names, token structure, Arabic rules or screen patterns, so it produced
convincing code that imported a component which did not exist, invented a prop, or
approximated a brand colour. Figma-to-code inconsistency had become
prompt-to-product inconsistency.
I had solved this for myself by writing specifications precise enough to build
from, but longer prompts were not a durable version of that. Asking everyone to
paste documentation into every session makes the work depend on memory again,
which is the problem the design system exists to remove.
The same prompt, with and without grounding. The failure is not that the model writes bad code; it is that the code looks right. A component that does not exist, a prop that was never in the API, and a brand colour close enough to pass review are all invisible until someone opens the library.
A read-only map of the repository
The MCP server exposes the live design-system repository to a coding agent
through 14 tools, none of which write. It does not generate the product or make
design decisions; it answers what is true before the agent starts guessing. An
agent can list and search components, read documentation, inspect source and
stories, retrieve and resolve tokens, and fetch the RTL, implementation and
screen-composition guides.
Four decisions in it are worth naming, because they are the difference between a
server that works and one that quietly goes stale:
It indexes the filesystem, not a manifest. Component discovery reads
directory entries and probes for the files that should accompany a component.
Adding a folder adds a component. There is no list to forget to update.
It reads live source when it can. If the package is running inside the
monorepo it walks up to the workspace root and reads the actual files; only a
published install falls back to a bundled snapshot.
The token resolver is a resolver. It merges core, semantic, extras, locale
and brand into one tree, then walks alias chains with cycle detection and
returns the whole chain, so the agent sees why a value is what it is.
Errors teach. A request for Buton comes back with "Did you mean: Button?"
rather than a failure, because an agent that gets a useful error corrects
itself instead of inventing a workaround.
Fourteen tools in three groups, and none of them write. The server opens the same files a contributor would: component source and docs, the token JSON, and the guides for RTL, screen composition and the implementation workflow.
01
IntentWhat the person asks for
Build a warehouse receiving screen
Add Arabic support
02
MCPWhat is true, and where the request belongs
Components
Source
Tokens
RTL rules
Design guide
03
Agent skillHow the request is interpreted
Choose a screen pattern
Plan the required primitives
04
Claude CodeThe implementation layer
Compose
Code
Run
Test
Correct
05
ProductA working result under the same constraints
React interface
Brand theme
LTR or RTL behaviour
The MCP is entered first and returned to throughout. Its own instructions route the request to the skill that should handle it, so the agent is placed in the right workflow before it has interpreted anything.
Components were not enough to describe a screen
The first version could answer "which Button variants exist?" That is useful and
insufficient. An agent can use Table, Card and Tabs correctly and still compose
them into a weak interface. It needs to know which surface owns scrolling, where
filters belong, and when a master-detail layout beats another modal.
DESIGN.md became the screen-level playbook: 639 lines defining a catalogue of
page patterns for the products we repeatedly build, plus navigation anatomy,
content density, spacing rhythm, motion and direction decisions. I split those
responsibilities into skills. Six are mine: screen assembly, which routes the
request, then page shell, navigation, content panel, motion, and a documentation
writer. A seventh is Emil Kowalski's animation skill, vendored deliberately and
pinned by hash in a lockfile rather than copied and forgotten, because motion
craft was not a wheel I needed to reinvent.
Every skill I wrote has a section listing what it does not do, and that constraint
matters more than the instructions. All four screen skills are forbidden from
creating components in the library, which stops a screen request from quietly
becoming an unreviewed addition to the shared system.
The four page shapes the screen guide names. Knowing that Table, Card and Tabs exist does not tell an agent which surface owns the scroll, where filters belong, or when a master detail layout beats another modal. These are the answers, written down once.
A list of components
The agent knew that Table, Card, Navigation and Modal existed, but not how Tagaddod combined them into a coherent operational screen.
A method for assembling products
Skills select a known screen pattern, retrieve the exact primitives, and apply shared rules for hierarchy, density, motion and direction.
The component library supplied the nouns. The design guide and skills supplied the grammar.
What happens when the library does not have it
No design system contains every component a product will need. The risky moment
is what happens after the agent searches and finds nothing.
I encoded that moment as a workflow. The agent searches again with related terms,
then tries to compose from existing primitives: a stat card may only need Card,
Number and Badge; a filter bar may be Popover, Select and Button. If composition
cannot provide the behaviour, it works through a fixed source order, a shadcn
reference ported without its styling, then Radix, then Base UI, then Ant Design,
before anything custom. The order matters more than any single entry in it,
because it is decided in advance rather than argued about per component.
Either path still obeys the system: the component stays in the consumer product
until it earns a place in the shared library, values come from tokens, and RTL is
part of the definition of done. That let the library stay opinionated without
becoming a blocker, and stopped "the design system does not have it" from becoming
permission to start a second visual language.
The absence of a component is a governed branch rather than an opening. Composition is tried first and most requests end there. The fallback ladder is ranked and named, so the choice is made in advance instead of improvised, and the same five rules apply whichever path the work takes.
Published to npm, then withdrew it
Chose
Source distribution from the private repository, with the MCP as the index over it
Trade-off
Consumers clone and link rather than install a version, so there is no version pinning and fixes have to be re-adopted by hand.
Thirty-one versions went to npm before I stopped. Publishing put an internal
system in public, which was not a trade the company wanted to make. What began
as a constraint turned out to suit the workflow better than npm had: an agent
reads real source rather than a built bundle, and vendoring a component
verbatim into a product became a supported path instead of a workaround. The
README states the relationship plainly: the MCP is only the index, and your
clone is the truth.
Documentation for three levels of prior knowledge
The workflow could not be useful only to people who already understood React,
tokens and MCP terminology. Foundations explain colour, typography, spacing and
motion. Component pages carry the API and live behaviour, generated from the same
preview files the library ships. A ten-chapter learning path introduces AI,
language models, Claude Code, development environments, GitHub, MCPs, and
frontend and backend concepts in plain language.
Documentation ended up being 35% of the repository by volume. For a system whose
consumers were a small team and their agents, that ratio is closer to right than
it looks.
Three levels of prior knowledge in one site. Foundations carries the decisions, component pages carry the contract, and the learning path starts from what an AI model is and ends at a working application. The brand and language selectors sit in the header of all three, so anyone reading the docs can see the system in either brand and either direction.
The Warehouse Center was the real test
Component previews prove a button renders. They cannot prove a system carries a
consequential product. That test came when the company replaced its Warehouse
Center, where field operations, physical inventory, quality control, transport
and customer fulfilment meet. I proposed rebuilding it and owned it end to end.
It is a case study of its own; what belongs here is what
it did to the design system.
The relationship is a shared language rather than a shared dependency. The product
carries its own copy of the token layer and implements its own components against
it, so the colour, spacing, type and direction are the system's while the
component code is the product's. That kept the product fast at the cost of
re-adopting fixes by hand.
Two tiers of navigation and a table, all from the library. The rail carries the top-level areas, the panel beside it appears only because the active area has sub-pages, and the content panel holds the Table with status rendered as badges.
The same screen and the same components in Arabic. The rail and its panel move to the right edge, the table columns reverse, and the type switches to Tajawal. Nothing here is a separate Arabic build: it is one composition rendered in the other direction.
Building the figures for this case study exposed three bugs, which is the honest
version of what a system in use looks like. The navigation rail lands on the wrong
edge in Arabic, because the component flips its own side in RTL while its CSS
already uses logical properties, so the two flips cancel out. The GreenPan theme
overrides four colour tokens rather than the full set, so a tinted surface keeps
the Tagaddod blue while the fill beside it turns green. And the brand shadow
recipes hard-coded a blue no theme could override, putting a blue halo under a
green button.
None of those appear in a component preview. They appear when a second brand, a
second language and a real screen are on at once, which is the argument for
building products with the system rather than only around it.
A reconstruction of the receiving screen, rebuilt from design-system components with invented order numbers, warehouses and weights, since the real screen carries live operational records. The Arabic is the product's own, lifted from its message catalogue. The mix is the point: navigation, panels, inputs, badges and buttons carry the library's tokens and behaviour, while the weighing sequence and the scale readout are composed in the product, because the library has no component for either.
What adoption actually looked like
It started by persuading three people to build with it while I watched, which was
the most useful week of the project. Watching someone else use a system you wrote
is the only reliable way to find the parts that are obvious to you and to nobody
else.
It ended somewhere I did not plan. On my way out of the company, the Chief
Product Officer asked me to run a session for product managers, engineers and the
product engineers shipping interfaces with agents, teaching them to work through
the design system. It had gone from something I talked people into, to something
the company wanted formally transferred before I left.
The visible difference was consistency. Products built independently no longer
inherited the defaults of whichever AI tool or starter template created them, and
nobody had to remember whether a prop was called tone, variant or intent.
Arabic changed more quietly. It stopped being the pass that gets cut when a
deadline arrives, because it was no longer a pass. It was in the tokens.
Outcome
505 tokens across four tiers, resolving two brands, two locales and two directions from one source
32 React component families composed over headless primitives, with 2,321 token references across 314 distinct tokens
A 14-tool read-only MCP server, six authored agent skills, and a 639-line screen-composition guide
Connecting an agent to the design system became the default way to start a new internal interface
Handed over through a company-wide session at the CPO's request
No controlled time study and no reliable design-drift baseline were ever run. Comparisons with earlier projects suggested faster implementation and fewer UI defects, and I am treating that as directional rather than publishing an invented percentage.
What I would not claim
Tests cover 16 of the 32 components, 21 files in total, and the suite is not
currently runnable because two dev dependencies are missing from the manifest.
There is no CI; the workflows that existed were removed and never replaced.
GreenPan is a real theme that works in the library and the documentation site,
but no product has shipped on it, so the second brand is exercised rather than
proven. And the twenty var() fallbacks still naming Tagaddod blue are a live
inconsistency, not a historical one.
Those are the marks of a system built by one person around a full-time role.
They are also the specific things a second contributor would have caught, which
is the honest argument for having one.
From a component library to an environment
The project began with familiar design-system questions. Which colours should we
use? How should buttons behave? How do we support two brands and Arabic? It ended
with a different one: how does a design system stay present when the person
building the product is describing intent to an agent?
The answer was not a bigger Figma library or a longer prompt. It was a chain of
connected sources of truth: tokens express the smallest reusable decisions,
components turn those into accessible behaviour, documentation explains when to
use them, screen guides define how components become a product, skills turn those
guides into repeatable workflows, the MCP keeps the agent grounded in what exists,
and products in use send new knowledge back to the tokens.
Each link is a real artefact rather than a stage in a process diagram: four token tiers, thirty-two component families, the documentation app, DESIGN.md, six authored agent skills, fourteen read-only MCP tools. The dashed return is the part that makes it a system rather than a library, and it is the link most design systems never close.
I did not teach an agent to have a designer's judgement. I built an environment
in which it had fewer opportunities to ignore that judgement. The most valuable
workflow was never the most autonomous one. It was the most constrained one:
inspect the real component, resolve the real token, follow the known pattern, run
the product, correct what fails.
Two things changed shape because of that. Building an interface stopped
requiring a frontend engineer in the room, because a product manager or a
designer could describe what they needed and get something that used the real
components and read correctly in Arabic. Mistakes still happened, and three of
them are named above, but they became the exception a review catches rather than
the default state of every new screen. And designing stopped having to begin in
Figma. For internal tools the design team could go straight to a working
prototype and keep going until the prototype was the product. The old relay of
design a screen, hand it over, wait, then reconcile what came back stopped being
necessary, and the decisions stopped degrading in transit because there was no
transit.
I set out to stop drift between Figma and code. What I built was a way for the
people closest to a problem to solve it themselves without the language of the
product coming apart on the way. It also changed what I think the job is. A
design system used to be the artefact I produced. Now I think of it as the
environment everything else gets produced in, and the measure is not how complete
the library looks but how hard it is to build the wrong thing inside it. When the
work reached a warehouse, a database and a physical scale, that environment still
held.