Design system · Case study 2026
Storybook Figma

Element Design System

A brandable design system. A brand is 22 dial positions, and everything else follows from them: variables, 41 components, dark mode, density, a Storybook and docs that AI tools can read.

Core, modern minimal: Geist, medium radius, thin grey lines, no shadow
Core · Modern minimal
Business, service brand: Figtree, pill buttons, grey card on a white page, soft shadow, deep purple
Business · Service brand
Play, gamified: Baloo 2 and Nunito, pill buttons with a solid bottom edge, uppercase buttons, green, pink and yellow
Play · Gamified
Brutalist, raw editorial: Archivo only, no radius, no card outline, black and greys
Brutalist · Raw editorial
One card, four brands. Same components and layout in Figma; only the Brand mode changes.
Timeline
October 2026
Ongoing
Role
Design System Designer
Team
Me, Myself and AI
Tools
Figma
Storybook
Claude
Claude Code

At a Glance

4
Brands built, from minimal UI to brutalist
22
Dials per brand, each with 2 to 8 positions
414
Figma variables, the single source for every brand
41
Components in seven groups, from forms to AI chat

The Idea

Most multi-brand systems treat the second brand as a patch. I wanted to see what happens when the brand is the input.

The usual setup: one brand is the default, and every other brand is a file that overrides some of its values. Whatever the file forgets falls back to the default without warning. Shape and spacing are often wired to raw scales, so a brand that wants square buttons also gets square avatars. And when AI tools generate UI from the system, they copy pixel values because that is what the components contain.

Element turns this around. A brand is a short list of named decisions — radius, line weight, depth, how much brand colour shows, type scale, label case, motion — and every variable, component and story is derived from those decisions. Components only ever read intent names such as radius/control or color/brand-text, never numbers. To prove it isn't four tints of one look, I built four brands that are deliberately far apart.

Research

Established systems get the documentation right and the brand model wrong

I studied how mature multi-brand systems, shadcn/ui and Material 3 handle theming, tokens and AI tooling. Three patterns kept coming up.

  • Brand colours baked into names. A token or button variant named after a colour ties the API to one palette. A green brand ends up with a token called purple.
  • Only colour is semantic. Shape, space and type are read straight from raw scales, so a brand can't change one component's corners without changing everything that shares the step.
  • Good agent docs, no checks. Some systems already ship guidelines written for AI agents. Nothing verifies that generated code actually uses the tokens.

Worth keeping: a description on every semantic token saying where it is used, inverse surfaces as a first-class context, and guidelines that people and AI read alike.

Override modelElement
A brand isA file of overrides on top of the default brandA set of named dial positions
A missing valueSilently falls back to the default brandStops the build
Shape and spaceRead from raw scalesRead from named tokens: radius/control, space/gap-related
Token namesCan carry colour namesIntent only: brand, secondary, tertiary, status
Density, schemeLight and dark at mostBrand × scheme × density, all independent

Dials

A brand is a handful of decisions, written down

Each dial has a short list of named positions, and one file decides what every position means in numbers. Choosing radius: lg sets control, container, media, badge and checkbox corners together, each capped by its own rule. Choosing depth: chunky gives buttons a thicker bottom edge they sink into. The brand file never contains a pixel value.

Voice and imagery are dials too, stored as text variables in Figma. Brutalist states facts and never sells; Play is a cheerful coach that says “Not quite” instead of “Wrong”. Those rules sit next to the radius and colour values, so a person or an AI tool writing copy for that brand reads the same thing. A Brand rules page in Figma shows every dial, voice and imagery rule side by side, bound to the variables, so it updates itself when a brand changes.

Brand rules page in Figma: 21 dials as rows and the four brands as columns, from neutral temperature and brand colour to button shape, colour variety and text density
The Brand rules page in Figma. Every value is bound to a variable, so the table changes when a brand does.

Four Brands

Every bug turned into a rule, and every rule into a shared token

Four very different brands break things one brand never would. Each fix went into the dials or a shared token, so it applies to every brand at once.

Focus ring keeps its shape

A drop-shadow ring didn't render on Figma component variants, so the ring became an outset stroke frame. Its corners use a new token, radius/control-ring (control radius + 4), so a square button gets a square ring and a pill gets a pill.

Errors never rely on colour

In thin-line brands, 1 px borders made an error field differ from a normal one only by colour. The active and error stroke now has a 2 px floor in every brand.

Checkboxes never read as radios

In round brands a 20 px checkbox looked like a radio. radius/inner is capped at 6 px, and Radio ignores the radius dial entirely: it is always a circle.

Fills never match the page

On tinted, layered pages the muted and disabled fills disappeared into the background. They now step one shade further, and secondary text is checked for 4.5:1 against both surface and muted.

Loan application form in the Core brand: amount, purpose, loan term radio cards, an optional note and a consent checkbox in its error state
Core
Loan application form in the Business brand: amount, purpose, loan term radio cards, an optional note and a consent checkbox in its error state
Business
Loan application form in the Play brand: amount, purpose, loan term radio cards, an optional note and a consent checkbox in its error state
Play
Loan application form in the Brutalist brand: amount, purpose, loan term radio cards, an optional note and a consent checkbox in its error state
Brutalist
The loan application pattern in all four brands. Radio cards stay rounded rectangles in round brands, the checkbox never reads as a radio, and the error state keeps a 2 px border even in thin-line brands.
The same heading and body styles in each brand, set in Estonian, English and Russian
Same text styles in every brand, in Estonian, English and Russian. Headings follow the brand; body text stays 16px everywhere.

Some things Figma can't express at all. It can't bind text case to a variable, so uppercase labels and buttons are applied in code and described on the Figma Label style. Easing and hover movement are the same. Anything that exists only on one side is documented on both.

How It's Built

Component rules

41 components, all following the same four rules

Before building past the first five components I wrote four component rules, so designers, developers and AI agents use the same words. They work like contracts in software: every component promises the same names, properties and states, and Figma, code and AI tools rely on that promise. Naming follows shadcn/ui. Properties are fixed lists: Variant is emphasis, Tone is meaning. States come from one list, and every interactive component has a Focus variant. Token levels say which tokens a component may use. A component that breaks a rule doesn't ship.

Component rules page in Figma: Naming and Props tables
The component rules have their own page in Figma and live in the repo as the same text.

Actions

Button, Icon button, Link

Forms

Input, Textarea, Select, Checkbox, Checkbox group, Radio, Radio card group, Chip group, Switch, Consent

Feedback

Alert, Toast, Progress, Spinner, Skeleton, Empty

Display

Badge, Avatar, Card

Overlays

Dialog, Alert dialog, Drawer, Popover, Tooltip, Dropdown menu

Navigation

Tabs, Accordion, Breadcrumb, Pagination, Stepper, Header, Side navigation, Separator

AI

Message, Prompt input, Reasoning, Sources, Suggestions

A home loan page in four brands built from Header, Breadcrumb, Tabs, Accordion and Pagination
Navigation components together on a loan page, in all four brands. Each frame only switches the Brand mode.

The system grows one component family at a time. Each family tests one dial and ends with a pattern page: the forms ship with a loan application, with validation, in all four brands. Each family is its own release, and a renamed property keeps working under its old name for one more release. The first rename turned Toggle into Switch and Button Variant=Danger into Tone=Danger, because danger is a meaning, not an emphasis.

Storybook
Switch brand, scheme and density on every component

Tokens

Four collections, and components only read the last two

Figma holds 414 variables: four token collections, plus 18 for the documentation itself. Each one has its own modes, so brand, scheme and density switch independently. Every public variable carries its CSS name (var(--el-radius-control)) and a description of where it is used.

Base 57
1 mode

Status palettes, focus width and offset, icon sizes

Brand 271
Core · Business · Play · Brutalist

Palettes, fonts, type sizes, radius, stroke, shadow, motion, dials, voice

Scheme 61
Light · Dark

color/* — the only colours components use

Density 7
Compact · Default · Comfortable

Control height and spacing

Colour roles as swatches: one row per brand and scheme, one column per role, from background and border to brand, link, focus ring and status colours
The same colour roles in every brand, light and dark. Status colours stay shared; everything else is the brand’s choice.
Raised, Overlay and Hover elevation styles in each brand, light and dark
The three elevation styles per brand. Business and Play add soft shadows, Core keeps a shadow for overlays only, and Brutalist draws a 1px outline around overlays instead.

There are no component-specific tokens. Button reads radius/control, never button/radius. When a component needs to look different, it gets a new shared token instead: badges got radius/badge, checkboxes radius/inner, tall fields radius/control-multiline so a textarea never turns into a pill, and fields radius/field so Business can have pill buttons next to square-ish fields. Component-specific tokens would let brands fork single components, which is exactly the drift a brandable system is meant to stop.

Figma and code

Figma is the source of truth, and it drifted from code within a day

Storybook doesn't keep its own copy of the design decisions. A snapshot of every Figma variable is pulled into the repo, a small script turns it into CSS variables, and the React components read only those. Figma's variables API is Enterprise-only, so the snapshot is pulled through the Figma MCP server instead.

1
Figma variables
Base, Brand, Scheme, Density
2
Snapshot
variables.json in the repo
3
CSS
--el-* per brand, scheme, density
4
Components
React + Storybook read tokens only

The drift showed up fast. My original dial build still had a finance brand that Figma had already replaced with another style. One day after the first snapshot, Figma had 374 variables instead of 364, and that brand's heading font had changed. Nothing was broken, but nothing told me either. It happened again when I reworked the four brands: I tuned them by hand in Figma first, then had to extend the dials until the build produced the same brands and push the result back.

That is the next piece: a token check that compares the Figma snapshot with what the code builds, lists every token that differs, and says which side to fix. Discipline doesn't keep two sources in sync. A check that runs on every change does.

AI layer

Docs an agent can read, checked against the tokens

A script reads every component from Figma and writes the documentation AI tools use: one main file with the rules and every token, a list of all components with their properties, states and tokens, one guideline per component, an index for AI crawlers and a skill that tells an agent how to build and review UI with Element.

The same question about repaying a home loan answered by the assistant in four brands, with citations, sources and suggested follow-ups
One question, four brands. Components and figures are identical; the assistant’s name, tone and disclaimer come from each brand’s voice variables.

The same script is the check. It stops when a component uses a token that doesn't exist, when a token has no CSS variable, or when an interactive component has no Focus state. Guidelines come straight from the Figma descriptions, so there is one text to keep right, not two.

Built with AI

Parallel AI sessions need shared rules as much as people do

Several Claude sessions built Element in parallel: one on the dials and Figma variables, others on component families in Figma, one on Storybook. They shared a single context file with the status, the decisions log and a rule for each area. A session could mark a page as “in progress, don't edit” and the others respected it.

It still went wrong once. One session overwrote the dial definitions with an older copy and quietly dropped five review fixes, including the 2 px error floor and the ring radius. They were merged back the same day, and the context file gained a rule: always read the current file before writing it. Intent-only token names did their job here too. AI-written components had no pixel values to copy, so a fixed token fixed every component at once.

Reflection

Constraints belong in shared tokens

Every rule that survived review — ring radius, checkbox corners, tall-control radius — became a shared token, not a per-component exception. That is what keeps four brands on one set of components.

Two sources drift in a day

Figma and code disagreed within 24 hours with nobody doing anything wrong. A snapshot plus an automated check is the only sync I trust.

Show range, not tints

Four brands that look alike prove nothing. A brutalist brand next to a gamified one exposed the hairline-border and round-checkbox problems that one brand never would.

What's Next

  • Token check between Figma and code, with a fix list per token.
  • Data components: Table, Data list, Description list, Stat, Filter and Date picker.
  • Estonian patterns: Smart-ID and Mobile-ID sign-in, personal ID code field, ET · EN · RU language switcher.

AI disclaimer.

  • Claude sessions built the Figma variables, components and Storybook from my direction, rules and reviews.
  • This case study was drafted with Claude from my project notes.