Skip to content

Design Infrastructure

Enterprise Design System

ROLE
Design System Owner, one of two designers using it, the rest of the team built on top of it
TIMELINE
~3 months to a working v1, then continuous growth alongside the product for roughly a year after
CORE PRODUCT
A 4-layer design system (Core, Library, Feature-Based, Sandbox) covering Persian and English, light and dark mode, and the full component library behind a live product
IMPACT
100+ components across 4 structured files, adopted by designers and frontend engineers outside the core team, still in active use

01 / Context & The Bottleneck

Why a design system, not the usual pitch for one.

The market this product competed in was loud, every competitor shipping fast, every team under pressure to look as sharp as the last release. That pressure runs straight through a design team of two and an outsourced frontend build. Outsourcing implementation means the people writing the code aren’t in the room for every design decision, and the gap between “what the designer meant” and “what got built” is exactly where products start looking inconsistent, not from carelessness, but from distance.

The actual pain points showed up early: a component re-explained in three different Slack threads because nobody had one source of truth to point to, a frontend engineer guessing at a spacing value because the Figma file had six versions of the same button. None of that is a design problem on its own. It’s a collaboration problem that shows up as a design problem.

The system was the fix for that specific gap, not an abstract commitment to “consistency.” It gave two designers and an external frontend team one shared reference instead of a conversation that had to happen over and over. Fewer threads, fewer guesses, fewer components quietly drifting apart the longer the product grew.

handoff.gap()
The gap between internal design decisions and outsourced implementation, and how a shared system closed it
variants_per_component(t)
Starting a design system before the first inconsistency exists, rather than retrofitting one after the fact

It didn’t launch complete. A basic foundation was in place from the start, but the system grew alongside the product, section by section, over roughly three months to a usable v1, and kept expanding for close to a year after that as new product surfaces demanded new patterns.

Who actually touched this system.

  • Two designers, myself and Hanie Mansour Kiai, building inside it day to day
  • A frontend team implementing components directly from it, following its naming conventions
  • Designers and frontend engineers outside the core team, who referenced or adopted parts of it
system.users
Design system usage spanning the core design team, frontend engineering, and outside contributors who adopted its conventions

Scope, stated plainly: Four structured Figma files hold the whole system: 24 foundational components in Core, 101 in the component Library, 77 feature-based components, and a full icon set. All of it built to support two languages (Persian and English), light and dark mode, and the full responsive range from mobile to desktop, without a single component duplicated per condition.

scope.count()
Component count across the four-file structure, Core, Library, Feature-Based, and the icon set

02 / Architecture & The Fix

A four-layer structure, not a flat component library.

The system runs on four layers, each with a distinct job, and each publishing into the next:

  • Core holds the raw building blocks, spacing tokens, typography, elevation, and base components. Nothing here is used directly in a product screen, it exists so everything built on top of it inherits the same foundation
  • Library is where usable components live, buttons, inputs, modals, and shared UI primitives, built from Core and carrying every state a component needs (default, hover, focus, disabled, error)
  • Feature-Based is where Library components get combined or customized for a specific product need, a component with a particular state, or several Library pieces assembled into one feature-level pattern
  • Sandbox is the final design file, where actual product screens and panels get built using everything the three layers below it provide

A change at the Core level doesn’t update every screen by itself, someone still has to go into Library and Feature-Based and apply it where it’s used. What the four-layer structure actually buys is scope, not magic. A spacing token or color value changes in exactly one place, and from there it’s a known, traceable update through two more layers, not a guessing game across forty scattered instances of the same button with no idea which ones are current. The system doesn’t remove the work of updating. It removes the work of figuring out what needs updating and finding every place it lives.

layers.publish()
Core, Library, Feature-Based, and Sandbox, each layer publishing into the next so foundational changes propagate without manual rework

What happens when a new side project enters the picture.

The four-layer structure isn’t locked to one way of extending. A new project can pull from the existing Library and spin up its own file on top of it, inheriting the foundation without touching the original system, or it can hook directly into the same Variables and update the shared file in place. Which path makes sense depends on the project and the organization: a one-off experiment that shouldn’t risk the core system takes the first route, a project that’s meant to grow into the same product family takes the second. The structure doesn’t force one answer, it leaves the decision open to whoever’s extending it.

extend(new_project)
Two ways to extend the system for a new project, a separate file built from the shared Library, or a direct hook into the same Variables

The whole system, in one picture.

Three token categories at the top (color, spacing, typography) feed into Core, which then publishes downward through Library, Feature-Based, and Sandbox. Nothing below Core touches a raw value, every layer inherits from the one above it.

architecture.map
System architecture — token system feeding 4 publishing layers

Built on atomic design principles, adapted to how the team actually worked.

The structure borrows from atomic design (atoms, molecules, organisms, templates, pages) but isn’t a rigid copy of it. The four-file system is the practical translation of that thinking into something a two-person design team and an external frontend team could both navigate without needing a design theory background to use it correctly.

atomic → layers
Atomic design principles adapted into the system's four-layer, four-file structure

What’s inside each layer, concretely.

  • Core holds spacing tokens, typography styles, elevation values, and base (unstyled) components, the raw material everything else is built from
  • Library holds the full set of usable UI primitives, buttons, inputs, modals, and shared patterns, plus the color and spacing definitions applied to them
  • Feature-Based holds product-level modules and custom variants, Library components combined or adjusted for a specific feature’s needs
  • Sandbox holds the actual product pages and designed experiences, the space where everything from the three layers below gets tested, iterated, and validated before anything merges into the main system
ls ./layers/*
What each of the four layers holds, tokens and base components in Core through final product pages in Sandbox

What building one component through all four layers actually looked like.

Most of the above is structural. Here’s what it looked like in practice, using the Button as the example, the clearest one to walk through end to end.

Inside Core, a button is built from exactly two pieces: one typography component and two icon slots (left and right). That’s it, no color, no size variants, no states. It’s the skeleton every other button in the system traces back to. A fab (floating action button) sits right next to it in the same Core file, sharing the same typography and icon logic, because a fab is structurally still a button with a different shell around it.

core/button.skeleton
The Core button, built from one typography component and two icon slots, shared as the structural base for both button and fab

In Library, that skeleton gets pulled in and multiplied across every dimension the product actually needs:

  • Style: Contained, plus Primary / Secondary / Inherit variants
  • Size: Large, Medium, Small
  • Color intent: Primary, Secondary, Error, Warning, Success
  • State: Enabled, Hovered, Focused, Disabled, Loading

Laid out as a grid, that’s dozens of distinct combinations, all generated from the same Core skeleton, all guaranteed to share identical spacing, type, and icon alignment because they’re built from one source, not redrawn per variant. Color never touches a raw hex value here either, the Loading state’s spinner, for instance, is filled with action/primary/text, not a picked purple, so if that token’s underlying value shifts, every loading spinner across every button variant shifts with it without anyone touching this file directly.

library/button · style × size × color × state
Full Button variant matrix in Library, style × size × color × state, all generated from the same Core skeleton

From there, Feature-Based pulls Library buttons into feature-level patterns, a specific button paired with a specific icon and label inside a form footer, a button combined with a tooltip for a disabled state that needs explaining. The button itself isn’t redesigned at this layer, it’s composed with other Library pieces into something reusable at the feature level.

By the time a designer is building an actual screen in Sandbox, the button isn’t a decision anymore, it’s a pick from the Library or Feature-Based set, dropped in with every state, every token, and every bilingual behavior already resolved. The work happened three layers upstream.

trace(button)
One component traced through all four layers, from a two-piece Core skeleton to a finished button dropped into a product screen

Color tokens, never used raw.

Base color values (a full palette, shade 50 through 900, across purple, orange, grey, and more) exist in one layer and are never referenced directly in a design. On top of that sits a semantic naming layer, tokens named for what they do, not what they look like: action/primary/state/hovered, action/error/main, surface/secondary, text/headline/darker. A designer placing a button never picks a shade of purple, they pick “primary button, hover state,” and the system resolves what that means.

tokens/color · base → semantic
Semantic color tokens built on top of a raw base palette, naming convention follows purpose rather than appearance

That same semantic layer is what makes light and dark mode a non-event instead of a parallel design effort. Each token resolves to a different base value depending on mode, action/primary/main is purple/700 in light mode and purple/200 in dark mode, but the design using the token never changes. Switching mode is a variable swap, not a redesign.

mode.swap(light, dark)
Semantic tokens resolving to different base values per mode, light and dark mode handled without duplicating component designs

Bilingual by construction, not by patching.

Most components handled Persian and English without extra work, since the token and spacing system was language-agnostic by design. The real friction showed up in components where content length is baked into the layout. Text fields with floating labels, for instance, where the label’s position and the surrounding spacing had to be calculated differently depending on whether the label text was a short English word or a longer Persian phrase, RTL and LTR behaving differently enough that the same component needed distinct handling for each language rather than a simple mirror.

textfield · ltr / rtl
Text field component example where label length and RTL/LTR behavior required distinct per-language handling rather than a simple direction flip

What that actually looks like, side by side.

The same sign-in component, Persian and English, light and dark, all four combinations built from the identical underlying structure. Nothing here is four separate designs, it’s one component resolving four different conditions through the token and layout system described above.

sign-in · {fa, en} × {light, dark}
One sign-in component across Persian/English and light/dark mode, built from a single underlying structure rather than four separate designs

One component, every screen size.

Responsive behavior was built into each component itself rather than maintained as separate mobile and desktop versions. A single button, card, or input adapts its sizing and layout logic across breakpoints, so a design change at the component level holds across the full device range instead of needing to be repeated per layout.

card · one definition, every breakpoint
Single component definitions handling both mobile and desktop layouts rather than maintaining duplicate versions per breakpoint

Naming conventions aligned with engineering, not decided in isolation.

Component and property names were set collaboratively with the frontend team early on, in a working session, then maintained consistently from there. Once engineering saw the naming held up and made their implementation faster, they followed the convention without needing to be asked twice, which meant handoff stopped being a translation step between design language and code language, it was the same language.

figma.props === code.props
Component and property naming aligned directly with the frontend team's implementation conventions, removing the translation step at handoff

Error handling and system feedback as first-class components, not an afterthought.

Error states, validation feedback, and system messaging were designed as part of the core component set from the start, not bolted onto individual screens as problems came up. Every input, form, and action carried a defined feedback pattern it could fall back to.

feedback/*
Error handling and system feedback designed as core, reusable component states rather than one-off solutions per screen

Design and UX principles applied as process, not as a slogan.

A working checklist guided every component through a consistent set of checks before it was considered done, covering things like contrast and legibility, touch target sizing for interactive elements, consistent spacing against the base grid, and state coverage (default, hover, focus, disabled, error) for anything interactive. The point wasn’t a single audit at the end, it was a standard every component had to clear before it entered the Library layer, so nothing inconsistent made it downstream into Feature-Based or Sandbox.

qa.gate → library
Working checklist applied to every component before it entered the Library layer, covering contrast, touch targets, spacing, and state coverage

What the Variables setup actually looked like underneath.

Beyond the visible component structure, the system ran on a layered Variables setup in Figma itself, Layout Spacing and the full color Palette defined once at the Core level, then separately referenced inside the component Library rather than redefined there. A component in Library doesn’t carry its own copy of a spacing or color value, it points back to the same shared variable Core already defined, alongside typography set by family, size, and typeface as their own linked variable groups. That’s what made the propagation described earlier actually hold up in practice, the dependency isn’t a convention designers agreed to follow, it’s wired into the file structure itself.

figma · local variables
Figma Variables panel showing Layout Spacing, Palette, and Typography (Family/Size/Typeface) referenced from Core into the Library, rather than redefined per file

Documentation lived inside the components, not in a separate guide.

Rather than maintaining a standalone documentation site that could drift out of sync with the actual files, every component carried its own short usage note directly in the layer structure, what it is, when to use it, right next to the component itself. A designer opening the Button layer sees its description before they even open the variant panel. That kept the documentation from going stale the way a separate wiki tends to, it lived exactly where it would be read, updated in the same breath as the component it described.

layers · Button
Component-level usage notes built directly into the layer structure, read at the point of use rather than in a separate, driftable guide

How a change actually got approved before it touched the system.

Nothing got added on instinct. A proposed change started with root-cause work, making sure the actual problem was understood before touching anything, then moved through research and alignment across design and the relevant teams. Only after that did a new design get built, tested on a limited part of the product first, and reviewed against real feedback. If it held up, it rolled out to the full system and went to engineering for implementation. A four-step gate (diagnose, design, pilot, roll out) stood between “this would be nice” and “this is now part of the system,” which is what kept the library from accumulating one-off exceptions no one remembered approving.

change.request → system
The approval path for a system change, root-cause diagnosis, design, limited pilot, then full rollout to the system and engineering

The icon system, at a scale most component libraries never reach.

Over 40,000 distinct icons, including regional flag variants, lived inside the system. Every icon shared the same base size, but the surrounding structure was built so each one resized automatically to fit whatever card, button, or container it landed in, rather than needing a manually resized version per context. At that volume, manual resizing per instance wasn’t a realistic option, the system had to handle it structurally or the icon set would have been unmanageable within weeks.

03 / Impact & Outcome

What the numbers actually show.

  • 100+ components across Core, Library, and Feature-Based, plus a full icon set, organized across 4 structured files
  • One token system covering two languages, two color modes, and the full responsive range, with zero component duplication per condition
  • Roughly 3 months to a working v1, then continuous growth alongside the product for close to a year
  • Adopted beyond the core two-person design team, by frontend engineers and outside designers who referenced or built on its conventions
impact.summary()
System scope and adoption summary, components, files, languages, and modes covered by a single token architecture

Where this sits against the industry.

Most startups don’t make it this far with a design system intact. Industry research backs that up directly: 68% of startups end up rebuilding their UI from scratch rather than scaling the one they started with (Figma Design Systems Survey, 2023). This one didn’t get rebuilt. It scaled.

That matters more than it might read at first. Most design system failures aren’t a single bad decision, they’re months of small inconsistencies that nobody catches until the whole thing needs a redo. Separately, mature design systems are reported to cut inconsistency fixes by roughly 60% once they’re established (UXPin DesignOps Report, 2023), and that tracks with what happened here, consistency stopped being something to chase case by case and became the system’s default behavior instead, baked into the token structure rather than policed screen by screen.

benchmark(industry, this_system)
Industry context on design system survival rates and inconsistency reduction, cited from Figma's 2023 Design Systems Survey and UXPin's DesignOps Report

What consistency at this scale actually bought the team.

The clearest signal wasn’t a metric, it was behavior. Designers and frontend engineers outside the core team started adopting the system’s conventions without being told to. People who worked alongside the project independently described it as the most cleanly systemized design work they’d seen on a team that size, unprompted, not solicited as a testimonial. That’s a harder thing to earn than a usage number, because it only happens when a system actually removes friction instead of adding process for its own sake.

adoption.graph
Independent adoption of the system's conventions by designers and engineers outside the core team, driven by the system working rather than by mandate

What it meant to own a system at this scale.

Most teams running a system at this scope have a dedicated design-ops function, sometimes a full team, just to keep tokens, documentation, and governance from drifting. Here, it was built and maintained inside a two-person design team, alongside actual product design work, not instead of it. Hanie Mansour Kiai joined partway through and carried changes forward alongside me from then on, which is part of why the system held up past the point where one person’s attention would have started to crack under it. A system loose enough to need constant babysitting doesn’t survive being run by two people on the side of their actual design work. This one had to be tight enough to mostly run itself, which is exactly what the four-layer propagation and the semantic token layer were for.

What this project actually proved.

That a two-person design team can build and maintain a system robust enough for a full product, two languages, two color modes, and a growing frontend team, without it ever needing a dedicated design-ops hire to hold it together. The system is still in active use. Some of the people who built alongside it moved to other teams or, in my case, left the company; the system didn’t. That’s the actual test of whether a design system was built right: whether it keeps working once the person who built it is no longer in the room.

The argument

What this system actually argues for isn’t a UI preference. It’s that systems thinking doesn’t stop at the interface, it’s the same discipline applied to how decisions, dependencies, and people compound over time, and a design system is just the version of that discipline you can see.