Skip to main content

Smallstep · 2022–Current

Making a design system AI-ready

Design engineer: 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. Rebuild on shadcn, the kit models already know, with a machine-readable contract between Figma and code.

Three decisions

1. A parity manifest as the contract

Every component gets an entry in a machine-readable manifest: its status, its deliberate deviations from upstream shadcn, and which legacy components it replaces. A second manifest resolves the fate of every legacy export: replaced, absorbed into a recipe, or deprecated. A test fails the build if the docs and the manifest ever drift.

"How's the migration going?" stopped being a feeling. It became a query. It is also the file an agent reads to know which component to reach for.

I directed Claude Code against that contract. The agent had to pick mapped components instead of inventing APIs. The same test I wanted any model to pass.

  • 32replaced
  • 8absorbed
  • 13deprecated
  • still moving
62 legacy exports, each with a fate.

2. The hard component: Table

Identity products live and die by tables. The legacy Table had one feature upstream shadcn does not: expandable rows. Rather than fork, I specced it as an additive part and directed the agent to build it: a toggle plus an expanded-row panel, controlled, animated with a CSS grid-rows 0fr→1fr transition, inert while collapsed. Every geometry decision (row heights, radii, sort-button sizing) was probed against the Figma kit, not eyeballed.

Design feedback landed as measured fixes. One "wrong background color" report was a button variant's aria-expanded style leaking through, invisible unless you diff computed styles.

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

3. The one I killed: a solid status variant

The Figma kit had a solid-fill variant for status indicators. Before writing any code I measured it: light-on-color text failed WCAG AA in three of four statuses. I prototyped darker inverted fills. They worked. The honest call was simpler: delete the variant.

The set got smaller. The accessibility gate stayed strict. 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.

The living system

The library ships weekly. Components land with Storybook stories, automated axe checks per story, Chromatic visual regression, and Figma Code Connect mappings so Dev Mode shows current code instead of pseudo-markup.

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 already reuses, written down with the rules a template can't enforce.

That work does not start until this migration is drained. I'll update this when it does.

© 2026 Renzo Bianchi

Last updated August26