# Sid-Kit

A React design system where color, depth and corners are computed rather than picked. Set an intent on a container and everything inside follows.

- Author: [Max Vyshniakov](https://maxvyshniakov.com/about/)
- Role: Design Engineer
- Period: 5 March – 20 October 2026
- Status: Version 0.35, in continued development, with a public catalog
- Models: GPT-5.5 (10.9M output tokens, 38%), GPT-5.6 Sol (6.8M output tokens, 24%), Claude Opus 5.5 (6.0M output tokens, 21%), Claude Fable 5 (2.2M output tokens, 8%), GPT-6 Astra (1.8M output tokens, 6%), Claude Opus 5 (0.8M output tokens, 3%), Other models (0.2M output tokens, <1%), Claude Opus 4.6 (not logged). Only sessions kept on this machine count: Codex from 10 April, Claude Code from 14 July. The March work in Studio isn't counted.
- Stack: React, TypeScript, CSS custom properties, OKLCH, Tailwind CSS, Base UI, shadcn, Storybook, class-variance-authority, Motion, cmdk, Vaul, Sonner, Embla Carousel, Recharts, React Day Picker, TanStack Virtual, Shiki, Vitest, Testing Library, Playwright, Oxlint, Bun, GitHub Packages, Cloudflare Workers, Agent Skills
- Category: Design systems
- Topics: Design systems, Interaction design, Accessibility, AI agents
- Web page: https://maxvyshniakov.com/work/sid-kit/
- Links: [Browse the Storybook](https://sid-kit.maxvyshniakov.com/), [Live demo: Sid-Kit: variants and intents](https://sid-kit.maxvyshniakov.com/releases/d8ec9066f9d3137d0abd1d565108c896269153d9/?path=/story/sid-kit-components-button--variants-and-intents)

By March 2026, Sirv AI Studio's components had drifted into local copies, and the coding agents writing new screens copied whichever version they saw most often, so every drift bred more. I reset the shared UI to canonical shadcn and built Sid-Kit on that baseline, a React design system where nobody picks a color. Every color comes from one OKLCH specification, a container's intent flows to everything inside it, and a card works out its own depth from where it sits. The real work was the system, its rules and keeping the coding agents on them: in June one of them rebuilt a dialog by hand because a stale line in its instructions still pointed it at plain shadcn.

![A matrix of Sid-Kit Save buttons in the dark theme. Columns are seven variants from solid to link; rows are the named intents neutral, stark, primary, danger, warning, success and info, then two generated ones, a hue of 280 and the key Ada Lovelace. Each row keeps its color across every variant.](https://maxvyshniakov.com/media/8ef5e2a2e08ecf0c3a46bfa9161722aa8047cd18814a0271e6ec4a95149b8a84.webp)

One button in every variant and intent, Sid-Kit 0.37.1.

## At a glance

* 534: contexts with identical colors after every color moved into one specification
* 132: releases, from 0.1.0 in May to 0.37.1 on 28 October
* 113: components and patterns in the public catalog
* about 1,500: unit and interaction tests, run in a real browser

- Studio's components had drifted from shadcn, and agents copied the drifted versions into every new screen. → [Reset to canonical shadcn before designing anything](https://maxvyshniakov.com/work/sid-kit/#agents-copy-whatever-they-see-most)
- Token names were inconsistent and colliding, and the colors behind them came from formulas that had drifted apart. → [Generate every color from one OKLCH specification](https://maxvyshniakov.com/work/sid-kit/#every-color-is-a-formula-in-one-file)
- A tinted panel had to hand its color to every label, field, badge and button inside it. → [Let intent and depth flow through the CSS cascade](https://maxvyshniakov.com/work/sid-kit/#intent-flows-down-the-tree)
- An agent built a dialog around the kit instead of with it, following a stale line in the repository's own instructions. → [Treat an agent's mistake as a bug in its instructions](https://maxvyshniakov.com/work/sid-kit/#an-agents-mistake-is-a-bug-in-its-instructions)

## Agents copy whatever they see most

With coding agents writing screens, a design system's first job is to give them one version to copy. Igor Varyvoda created Sirv AI Studio in December 2025 and built most of it. In February he moved its interface to shadcn. The product grew fast, which was the right call for a new product, but by March the components had drifted: edited classes, convenience wrappers, variants inside primitives and global CSS overriding component styles.

Each local change made upstream updates harder, and it taught the agents the wrong lesson. An agent writing a new screen copies the pattern it sees most often, so every drifted copy produced more copies.

On 5–7 March I took ownership of the shared UI and reset it in one branch. A script fetched 33 components from the official shadcn registry. A codemod rewrote 167 tooltip usages in 63 files. Global overrides went away, and the customizations worth keeping moved out of the primitives into documented places. Igor added hooks and a skill that held the agents to the canonical components, fixed the violations they turned up, merged the branch and fixed the tests the migration broke.

Problem: Studio's components had drifted from shadcn, and agents copied the drifted versions into every new screen. **Solution: Reset the shared UI to canonical shadcn before designing anything of my own.** Cost: The local customizations were thrown away or moved out of the primitives, and the migration broke tests that Igor had to fix. Result: Agents had one version of each component to copy, hooks flagged any departure from it, and everything in Sid-Kit is built on that base.

By May the kit had outgrown its starting point, and I asked whether it was worth making reusable:

> It's already better than default shadcn, and heavily refined in places. And there would be one place to make fixes, with a Storybook.
>
> — Me to Codex, 13 May

Six minutes later it had a name, Sid-Kit. That day it became a package inside Studio, and the next day Sparta, the B2B platform I was building, moved onto it too.

## Nobody picks a color

Sid-Kit components never name a color. They paint with roles, so a list of categories gets consistent colors in both themes without anyone choosing them. Data-heavy products need a lot of meaningful color, a status per order or a badge per category, and a palette picked by hand works in one theme and fails in the other.

A role is a job: the solid fill of a primary button, a soft fill, the quietest fill of a selected row, text in the intent's color, muted text, text placed on the fill, borders in four strengths, each with its hover state. Every role exists for every intent. The named intents are neutral, stark, primary, danger, warning, success and info, and any other can be generated from a hue or from a key such as a customer's name.

![A table of color roles against intents in the dark theme. Rows are fill, hovered fill, soft and subtle fills, intent text, muted text and text on the fill; columns are neutral, primary, danger, warning, success, info and an intent generated from the key Ada Lovelace.](https://maxvyshniakov.com/media/be6b921019bf2e93c4ed5d6cfc527571db2527d19a46e4c7e0d23338a1cbb272.webp)

Part of the role table, with an intent made from a name.

[Open the live demo in Storybook: Sid-Kit: color roles](https://sid-kit.maxvyshniakov.com/releases/d8ec9066f9d3137d0abd1d565108c896269153d9/?path=/story/sid-kit-styling-tokens--colors)

## Every color is a formula in one file

Since September every color in Sid-Kit comes from one specification of OKLCH seeds and formulas, about 460 lines, and the move changed nothing a user could see. It started on 23 September, with a list of token names on my screen:

> I see an inconsistency in how our tokens are named. Card intent has the intent at the end, but then it's intent soft, intent subtle. What do you think about how our intents are named, how they're applied as modifiers, and how it should be?
>
> — Me to Codex, 23 September

The next day I asked Claude to analyze the naming and the formulas behind it, and gave its plan to Codex for review. Codex confirmed the collisions and the diverging formulas, backed a single source and doubted that cutting public tokens was justified. It also named the trap that would shape the cascade: CSS computes a derived color where it is defined and children inherit the result, so a coefficient changed on a child recomputes nothing. I made three calls: "Make foreground neutral an alias of foreground. Get rid of legacy completely. Leave contrast for now." For the rest I said plainly that I didn't know how risky it was or how much of my attention it needed, so the plan had to prove itself.

It did. A build step generates the token stylesheet of more than 3,000 lines, the Tailwind bridge and the typed intent roles from the specification, and fails if they are stale. When the specification replaced the hand-written palette, every resolved value stayed identical in all 534 contexts checked. The old names went the same evening, as a breaking change, with a script that rewrites consumers.

Most tokens are formulas rather than values. A hovered fill is the fill with its lightness and chroma scaled by two named coefficients, so changing a coefficient moves every hover state at once. The catalog's Raw Variables page reads the same stylesheet and expands each definition one `var()` at a time, down to the numbers the browser computes, so it can't drift from the components.

![The Raw Variables page in the dark theme. A sidebar groups tokens into surfaces, fills, text, borders and shadows; the Solid fills panel lists --fill-intent and --fill-neutral with their hovered states and subtle fills mixed over the background, each with a color swatch and its definition expanded step by step, for example from var(--fill-neutral-hovered) through oklch(from var(--fill-neutral) calc(l * var(--k-intent-hovered-l)) ...) to oklch(from oklch(0.50 0 0) calc(l * 1.1) calc(c * 0.9) h).](https://maxvyshniakov.com/media/e55c3bed31804904c69e4a7b756ef5833969fbe303849222d404d2a1c40cae93.webp)

The solid fills, expanded from the stylesheet itself.

[Open the live demo in Storybook: Sid-Kit: raw variables](https://sid-kit.maxvyshniakov.com/releases/d8ec9066f9d3137d0abd1d565108c896269153d9/?path=/story/sid-kit-styling-tokens--raw-variables)

Problem: Token names were inconsistent and colliding, and the colors behind them came from formulas that had drifted apart. **Solution: Generate every color from one OKLCH specification, and drop the old names outright.** Cost: Breaking renames for every consumer. The naming grammar changed several times on the way, and 25 of the 128 releases so far carry breaking changes. Result: One place for every color decision, a grammar of names that reads the same in every role, and a refactor proved by 534 unchanged contexts.

## Intent flows down the tree

An intent is not a property of one component. Set it on any container, and the roles inside resolve against it: text, dividers, fields, badges and buttons all follow. A component without its own intent takes its container's; one with an intent keeps it. Since 0.35 that is the default for every primitive, and a grey look has to be asked for with an explicit neutral intent.

![Sid-Kit comboboxes in the dark theme in four tinted panels, neutral, primary, danger and a hue of 305, each with an empty field, a chosen Next.js and a Next.js with a clear button. Labels, fields, chevrons and clear buttons take the panel's color; the last field is focused with a violet ring and its list is open below it.](https://maxvyshniakov.com/media/7d0e91a8a430ace1073583253709b169fab9315fc1cc120a192e6e111b918ee0.webp)

The same comboboxes in four tinted containers.

[Open the live demo in Storybook: Sid-Kit: intent inheritance](https://sid-kit.maxvyshniakov.com/releases/d8ec9066f9d3137d0abd1d565108c896269153d9/?path=/story/sid-kit-components-combobox--intent-inheritance)

Nothing in the card below names a color either. The markup is identical in all six; only the container's intent and the theme change.

![The same Spring campaign card three times, in the dark theme above and the light theme below: neutral, success and danger. Titles, metadata, the In review badge, the file name field and the Approve and Comment buttons all change color with the card.](https://maxvyshniakov.com/media/a287de32532f847f65136a611bd212ebf8e83086724943641d7183d36d5da3c9.webp)

One card, three scopes, two themes.

[Open the live demo in Storybook: Sid-Kit: surfaces and intent scopes](https://sid-kit.maxvyshniakov.com/releases/d8ec9066f9d3137d0abd1d565108c896269153d9/?path=/story/sid-kit-styling-tokens--surfaces)

It is ordinary CSS inheritance, and it has the price Codex warned about. A token derived from an inherited one would freeze the parent's value, so every scope recomputes its dependent tokens locally, and a test fails if a scope forgets one. And because inheritance does not cross a React portal, the content of a popover or menu is handed its scope explicitly.

Problem: A tinted panel had to hand its color to every label, field, badge and button inside it. **Solution: Let intent flow through the CSS cascade instead of passing colors through props.** Cost: Every scope restates its derived tokens, a test guards the list, and portals need their scope passed by hand. Result: Markup never names a color. Change one container's intent and everything inside follows, in plain HTML as well as in React.

## A card works out its own depth

A card should look raised on a page, and a card inside it raised on the card. In most systems someone picks a grey for each case. In Sid-Kit, elevation is a CSS context from 0 to 4. A card sits one level above whatever it sits on, through any wrapper. A popover starts at level 2, or one above its parent.

![Nested cards in the dark theme, each labelled with its elevation, lightness and chroma: a card at level 1 holding a card at level 2 and a region that keeps level 1, and a popover at level 2 holding cards at levels 3 and 4.](https://maxvyshniakov.com/media/30eea72b6e6abf792fe63b9cc1cdd1e7c84b90c341cfe900771558dc341fc551.webp)

Nested cards, each reading its level from where it sits.

[Open the live demo in Storybook: Sid-Kit: elevation](https://sid-kit.maxvyshniakov.com/releases/d8ec9066f9d3137d0abd1d565108c896269153d9/?path=/story/sid-kit-styling-tokens--elevation)

Each level picks a lightness and a chroma limit, and the intent supplies the hue. The chroma limit came from looking: with one constant for every level, the lowest surfaces were too saturated and the highest looked washed out, so I asked for chroma to follow elevation too. Dark surfaces get lighter as they rise; light surfaces alternate white and off-white. Fields paint one step above the surface they sit on, so a form looks right on a page, a card or a popover without a separate variant.

![Five elevation levels in a row, twice: in the dark theme they grow lighter from level 0 to level 4; in the light theme they alternate white and off-white.](https://maxvyshniakov.com/media/2941721f668580c43934f49851c256e076be7fa882d80333766181a961efc95f.webp)

The same five levels in both themes.

[Open the live demo in Storybook: Sid-Kit: elevation levels](https://sid-kit.maxvyshniakov.com/releases/d8ec9066f9d3137d0abd1d565108c896269153d9/?path=/story/sid-kit-styling-tokens--elevation)

All of it hangs on one class. A `surface-*` preset works out its level, paints the background and hands everything inside roles for the hover, the text, the muted text and the border. A card inside a dialog comes out one level above the dialog, and a dropdown opened from that card lands above both, without anyone overriding a background or a border.

![Six surface presets as dark tiles, each with its class name, a one-line description and the roles it sets: surface-background for the page, surface-card for a raised card tinted by the intent, surface-card-primary with a fixed intent, surface-popover for menus and dialogs at level 2 or higher, surface-sidebar and surface-item for a selected row or chip.](https://maxvyshniakov.com/media/0e83e5234d0d5a834273770d19367aae24dc10fcb384408e1c07d46b27e074a9.webp)

Six surface presets and the roles each one hands down.

[Open the live demo in Storybook: Sid-Kit: surface presets](https://sid-kit.maxvyshniakov.com/releases/d8ec9066f9d3137d0abd1d565108c896269153d9/?path=/story/sid-kit-styling-tokens--surfaces)

Translucent controls follow the surface too. Soft fills and outlines are see-through on purpose, which turns wrong over a photo. `paint-opaque` mixes each translucent fill with the surface underneath through `color-mix`, so the control turns opaque and looks exactly as it did; `paint-opaque-border` does the same for bare edges.

![Soft, outline and dashed buttons in primary, danger, success and neutral over a checkerboard, in the dark theme above and the light theme below, in three columns: a plain div, one with paint-opaque and one with paint-opaque and paint-opaque-border. In the first column the checker shows through the soft fills and the borders; in the second the fills are solid; in the third the borders are too.](https://maxvyshniakov.com/media/9ebe3ab9f306ce9e4b8fd0940c2feb5ef898d6aeec5a45cdb0e46b6d08f007ba.webp)

The same buttons over a checkerboard, plain and opaque.

[Open the live demo in Storybook: Sid-Kit: opaque paint](https://sid-kit.maxvyshniakov.com/releases/d8ec9066f9d3137d0abd1d565108c896269153d9/?path=/story/sid-kit-styling-tokens--opaque-paint)

The edges are computed as well. Each shadow stacks eight layers on eased curves, after Tobias Ahlin's layered shadows and Philipp Brumm's generator, behind a half-pixel hairline in the intent's border color. Corners are superellipses, scaled by 1.5 to read as the same size as a round corner, which is what a browser without support gets.

![Pairs of Apply buttons on dark cards: on the left each radius step drawn as a superellipse with its border-radius and corner-shape values, on the right the same step drawn as a classic round corner.](https://maxvyshniakov.com/media/79c0945c548993e13e29628184bc5aa74462ad4b10fa81b46d1e1de0acd54307.webp)

Radius steps as superellipses and as round corners.

[Open the live demo in Storybook: Sid-Kit: radius](https://sid-kit.maxvyshniakov.com/releases/d8ec9066f9d3137d0abd1d565108c896269153d9/?path=/story/sid-kit-styling-tokens--radius)

## The interaction adapts, not only the layout

Responsive design usually changes the container. In Sid-Kit the interaction model itself adapts to the input and the space. AdaptiveSurface renders one feature body as a popover, dialog, sheet, docked panel or sticky column on desktop, and as a drawer or sheet on a phone, switching on viewport width, touch input or its own container's width rather than guessing device categories.

AdaptiveMenu does the same for actions. One list of entries drives a trigger menu, a context menu and a touch drawer. On desktop, submenus fly out. On touch, nested levels become a navigation stack: each level keeps its scroll position, focus returns to the control that opened the level, and the back gesture closes a level instead of leaving the page. A touchscreen laptop never swaps an open drawer for a dropdown: the presentation is chosen when the menu opens. In the demo, open the Share submenu or try the keyboard; on a touch device the menu becomes the drawer.

![The same asset actions twice. On the left, a desktop window with a compact menu open under an Actions button. On the right, a phone with a bottom drawer: a handle, the title Asset actions and larger touch targets, followed by Delivery and Metadata groups.](https://maxvyshniakov.com/media/c0a4f13813814a28c6878b39e57c1721ca89bbb087dff1bb623bdd5667297c8f.webp)

One list of entries, as a desktop menu and a phone drawer.

[Open the live demo in Storybook: Sid-Kit: adaptive menu](https://sid-kit.maxvyshniakov.com/releases/d8ec9066f9d3137d0abd1d565108c896269153d9/?path=/story/sid-kit-components-adaptivemenu--default)

Dismissal needed precision of its own: a press outside closes the menu without activating whatever sits underneath, and Escape closes only the topmost drawer. Two races, one that closed two layers at once and one that swallowed the next press nearby, are fixed and covered by tests.

## An agent's mistake is a bug in its instructions

When an agent breaks the system, I look for the instruction that let it, because fixing the screen only fixes one screen. On 9 June a Codex agent building a dialog in Studio assembled its layout by hand: its own padding, its own body wrapper, a footer with its own border.

> Something went wrong in this dialog: the header padding, the footer border. Why isn't it canonical, like in the Storybook? How do I get you to build canonical ones, with nothing of your own?
>
> — Me to Codex, 9 June

It fixed the dialog in a minute and admitted it had bypassed the kit's contract. I wanted to know why. Was a skill out of date? The agent said no and blamed its own priorities and a loose instruction, which it quoted: "use existing ShadCN primitives". I asked where that line came from, since we had long been on Sid-Kit, not plain shadcn. It was in the repository's own instructions for agents, a leftover from before the kit. When I said to go ahead, the agent replaced it within two minutes: Sid-Kit owns the composition, and its Storybook story is checked before any change in an app.

The fix that morning was one line; the lesson took the summer. By July every component had a Markdown contract of its own, rendered in Storybook for people. In September the contracts shipped with the package as a skill for the agents of every app that uses it, with an installer that fails when a project's copy is out of date. A lint policy flags restyled components. When I noticed each project restating the same Sid-Kit rules in its own push skills, the rules moved into the kit as one push gate, enforced by a hook. After the catalog was polished for the public on 19 October, I asked for a retro on the contracts and the skill themselves.

Problem: An agent built a dialog around the kit instead of with it, following a stale line in the repository's own instructions. **Solution: Treat an agent's mistake as a bug in its instructions, and ship the instructions with the kit.** Cost: A contract for every component, plus a skill and lint rules, all to keep current with every release. Result: Agents in any app read the same contract the Storybook shows people, and a stale copy of the rules fails a check instead of teaching an old habit.

## Checks run in a real browser

A simulated DOM can't resolve OKLCH, the cascade, real layout or pointer input, so Sid-Kit is checked in a real browser. Every story renders in headless Chromium with console-error and axe checks against a recorded baseline, and dozens of scripts test specific behavior such as menu dismissal. Before release, a packed build is installed into a temporary project outside the repository, server-rendered and built, because a component working in its own repository doesn't prove that an app gets the same bytes.

Contrast is measured rather than assumed. Leaving it for later didn't mean ignoring it: that night a script began checking every text-carrying pair against 4.5:1, modeling how Chromium clips out-of-gamut OKLCH. The pairs that fall short are listed rather than hidden: 52 today, 31 of them in the light theme, the lowest being text on a light info fill at 2.89:1. They are open palette decisions, and the list is a ratchet. A new pair below 4.5:1 fails the tests, a listed pair that gets worse fails, and one that reaches 4.5:1 has to come off the list.

These checks cover specific contracts and paths. They are not a claim that every combination of screen reader, device and app has been validated.

## Five apps, three of them mine

Sid-Kit runs in five apps, and who chose it matters more than the count. Two are Studio's: the server-rendered web app it was born in, and the Shopify app. On 24 May, eleven days after the kit got its name, Igor moved the Shopify app onto it on his own, which gave the system a second author and a context I hadn't designed for. The other three are mine: both of Sparta's apps and the editor of [Straylight](https://maxvyshniakov.com/work/straylight/), my Electron app.

My own apps did most of the shaping. After my contract with Sirv ended in June, I kept developing the kit inside Sparta, with 221 commits in July and 144 in August; most of the intent cascade, the surfaces and the elevation model were built there. In September it moved into a repository of its own, and it went from 0.23 to 0.35 while Straylight was built on it. Straylight's activity feed became a virtualized Timeline in the package, and a build quietly picking up a neighboring checkout led to pinned artifacts and an explicit refresh command.

The useful result is a place to fix behavior once: a dismissal bug or a contrast regression is repaired in the package, and every app gets the fix with its next update.

## How it was built

Sid-Kit was built with coding agents. In its own repository Claude Opus 5.5 co-signed 200 of the 326 commits; inside Sparta, Claude models co-signed 151 of the 516 that touched the kit, and Codex sessions on GPT-5.5 and GPT-5.6 Sol wrote most of the rest. What the code had to obey was decided by hand: the reset, the name, the model of roles, intents and surfaces, the calls on what to break, and the eye that caught drifting names and wrong chroma. When the agents' plans disagreed, I put them side by side; when one went wrong, I looked for the instruction behind it.

**Seven months at a glance**

* 5–7 Mar (2026-03-07): **The reset.** 33 components go back to canonical shadcn in one branch.
* 13 May (2026-05-13): **A package and a name.** The shared UI becomes Sid-Kit, in Studio and, the next day, in Sparta.
* 24 May (2026-05-24): **A second author.** Igor moves Studio's Shopify app onto the kit on his own.
* 9 Jun (2026-06-09): **The dialog built by hand.** A stale line in the agents' instructions is found and replaced.
* Jun–Aug (2026-07): **Inside Sparta.** The cascade, surfaces and elevation take shape.
* 23–24 Sep (2026-09-24): **One specification.** A naming grammar, every color generated, 534 contexts unchanged.
* 20 Oct (2026-10-20): **Version 0.35.2.** The catalog goes public.

* 326 — commits in its own repository, 200 co-signed by Claude Opus 5.5
* 516 — commits to the kit while it lived inside Sparta
* 771 — stories in the current Storybook
* 52 — known contrast pairs below 4.5:1, listed rather than hidden

Explore the full catalog: [Sid-Kit Storybook](https://sid-kit.maxvyshniakov.com/). The densest component built on Sid-Kit has a case of its own. [Read the Data Grid case study](https://maxvyshniakov.com/work/data-grid/).
