Skip to main content

Smallstep · 2026 · Design system

Making a design system AI-ready

Figma kit, component library, and the contract between them

smallstep.com

The problem

The UI library had no system. Figma and code had drifted so far that neither was a source of truth. Then a new constraint arrived: AI agents write frontend now. An agent cannot use a design system it cannot read.

Smallstep is identity and PKI: certificates, ACME, device attestation. The screens are dense tables, status states, and cryptographic detail. Years of accretion sat under them: multiple hardcoded sizes of the same component, layout primitives with magic numbers, form components welded to one forms library. Private, idiosyncratic APIs made the model guess. Guessing is drift.

We did not redesign the UI. We made the system readable to people and to models. We rebuilt on shadcn, the library models already know, and put a machine-readable contract between Figma and code.

My part

I have worked with Smallstep as a contractor since 2022. On this project I led the work end to end: scoped the migration, defined the parity manifest that became the contract, specced each component against the Figma kit, directed Claude Code to build against that contract, and decided what got through. The engineering team weighed in on decisions that touched the wider codebase. I owned the rest.

How the system grew

The plan had two halves. Rebuild on the stack the models already know, so the code an agent writes is likely right. Then add a thin layer that makes it verifiably right: a contract per component, a closed set of tokens, lint that runs in CI, and an index a model reads before it writes. Everything else was an amendment, each one a decision taken when the first version of the system met real work.

The contract came first

Before porting anything I had every legacy export audited and given a fate: replaced by a new component, absorbed into a recipe, or deprecated. What nobody used was deleted, not migrated. The result is a machine-readable manifest that maps each component to its Figma counterpart and its status, with a test that fails the build if the docs and the manifest ever drift.

For Smallstep, “how’s the migration going?” stopped being a feeling and became a query. For an agent, it is the file that says which component to reach for, so it picks a mapped one instead of inventing one.

  • 39replaced
  • 8absorbed
  • 14deprecated
  • 1kept
62 legacy exports, each with a fate. Snapshot of 7 Oct 2026.

The kit became the source of truth

After the first wave I changed how components were made. One component per pull request, because batches were slow to review. Each component authored in the Figma kit first, then built to match it. And once kit and code agreed, a Code Connect mapping, so Dev Mode hands a model the real component and its props.

Figma’s Code Connect panel for the Button component, marked Connected: a link to src/next/button.tsx and its Button export, then a code snippet importing Button and rendering it with variant and size props.
Code Connect on the kit’s Button: the real module and export, and the snippet a model gets, props included.

That closed the loop Smallstep wanted: design a screen in the kit and a model can reproduce it with the real components, reading the mapping instead of guessing the geometry.

A gate before code

Each kit page carries a status marker, so migration progress reads from the Figma sidebar. Then, after a few pull requests came out of drafts nobody had reviewed, the gate got teeth: a new component, axis, or variant enters the kit as a draft on its own page, one issue filed per gap, and code starts only after the kit owner promotes it. The comfortable alternative was to let the code land and review the design in the pull request. I rejected it.

The kit stays the spec, and nothing in the library is built from an assumption about the design.

Figma pages panel of the kit: a run of component pages each marked in parity, Top Navigation marked kit-ready, Typography unmarked, then Assets, Layouts, Blocks, Utility Components and a Drafts page.
A stretch of the kit’s pages in the Figma sidebar, as the sync with code was being marked. The status travels in the name; new work waits in Drafts.

The process, written down for the agent on both sides

The agent that builds components loads a skill before it starts: the rules distilled from review rounds and failed checks, a budget that stops it when the same failure repeats, and a list of what no automated check can reach, such as screen reader behavior or whether a motion feels right. Those stay with a person.

The agent that consumes the library gets its own skill: the instructions a new hire would get, a usage doc per module, lint whose error message names the size, variant, or part to use instead, and the rule that a suppressed lint without a filed gap is an unfiled gap.

warning  Button owns its padding. Use a size (default, xs, sm, lg, icon, icon-xs, icon-sm, icon-lg), or margin here and gap on the parent; one size per group, since a row of buttons shares one height. See dist/docs/usage/button.md, Sizes.  shadcn/no-restyle
One lint contract, verbatim. The message is the usage doc’s own words.

This is the part Smallstep gets the most from. A product team with an agent builds screens on the system without a designer in the loop for each one, and the system catches drift before a reviewer has to.

  • llms.txt
  • 62usage docs
  • 57Code Connect files
  • 8guard tests
What ships with the package, for people and for models.

Measured, not eyeballed

Between 7 Sep and 1 Oct 2026, every module in parity was audited against the kit on nine axes: layout, typography, colors, spacing, shadows, borders, radius, icons, and states. Each grade sits in the manifest next to the kit version it was measured against, and eight guard tests fail CI when docs, manifests, and code drift.

  • 59modules audited
  • 428pass
  • 99minor
  • 4moderate
  • 0critical
531 grades. The four moderate ones are named in the manifest: button and checkbox states, cert-parser and filters spacing.

Table is where the habit started. Identity products live and die by tables, and the legacy one had a feature upstream shadcn does not: expandable rows. I specced it as an additive part rather than a fork, and every row height, radius, and sort-button size was probed against the kit before it shipped.

Expandable rows as an additive part, not a fork. Storybook, mock data.

Measuring also cuts. The kit had a solid-fill variant for status indicators; light-on-color text failed WCAG AA in three of four statuses. I prototyped darker inverted fills and they worked, but the honest call was simpler: delete the variant. The same audit caught a contrast issue inherited from upstream shadcn’s palette and fixed it at the token layer, so every component benefited at once.

Status indicators (Default, Info, Warning, Error and Success) as soft tinted pills, shown in the light and dark themes.
The set that survived: soft fills, light and dark.

Proof it holds

A second product, built by one engineer with an agent, imports 229 named exports from the new namespace across 51 files. Zero imports from the legacy entry point. Two lint suppressions in the whole app, both filed as kit gaps instead of patched around.

The library itself was built the same way: 496 commits since July, 415 of them co-authored with Claude, across 217 merged pull requests.

The first screens agents built against the finished contract came out far better than what the team was getting before: mapped components instead of invented ones, consistent with the kit without a round of fixes. That is what lets the team ship features faster and at higher quality. I don’t have a number for how much faster yet, or for how many consistency bugs never happened, and I won’t invent one.

A filter builder with two top-level rules (Status is Expiring soon, Platform has any of 2 selected) and a nested group with two rules joined by Or, plus Add filter, Add group and Clear all controls.
Filters, with the advanced builder open. Storybook, mock data.

The living system

Releases are cut from conventional commits, 148 of them since July, 0.106 to 0.184. Components land with Storybook stories, automated axe checks per story, Chromatic visual regression, and their Code Connect mapping, so the kit, the code, and Dev Mode move together.

A CodeBlock component rendering a collapsible JSON example with expand-fields and copy controls, shown in the light and dark themes.
CodeBlock, one of the components the library ships, in both themes.

Beyond components

The contract makes components and tokens readable. Page layouts, when a change auto-saves, how a destructive action confirms: those still live in shipped screens and in people’s heads. Give the same ticket and the same library to two agents and you get two plausible, different pages.

The next layer is the layouts Smallstep reuses, written down with the rules a component can’t carry. It has started: the kit ships two page templates, content and sub-navigation, and the code follows them.

The map is closed. What’s left is call sites: five deprecated exports still live in shipped screens and get rewritten page by page. The system is already doing its job. Screens agents build on it come out consistent with the kit and with each other, and review goes to the product decision instead of the spacing. That was the bet, and it is paying off.

A page template: breadcrumb, a page header with Export and Add device actions, three tabs, an Add filter button and a sortable table of devices with status badges.
The content template, from the kit. A layout, not a screen from any app.

Next

If the interface is how your product works, write me.

© 2026 Renzo Bianchi

Last updated