Rules Lived in My Memory
Screens matched each other because I remembered the rules, not because the rules were documented, which meant a second contributor, whether human or AI, could not have reproduced my decisions.
For LendSight, a benchmarking tool for community banks and credit unions, I set up a design system to make sure the UI stayed consistent through every phase of the AI-assisted build. The system kept the same rules and tokens applied across the product lifecycle, from the Figma designs to the Storybook documentation to the code that Claude generated.

LendSight is a benchmarking and predictive modeling platform for community banks and credit unions, and I designed the product from 0 to 1. I was the only designer on the team, and most of the front-end code came from an AI build loop that went from Figma to Claude Code to React. The loop moved quickly, but it could only be as consistent as the rules it was given, so I built the design system to write those rules down in a form that both people and models could follow.
As the product grew, I kept running into the same three problems. The root cause was the same across all three: because values were hardcoded rather than assigned a role, there was no way to tell why a button was that blue, only that it was.
Screens matched each other because I remembered the rules, not because the rules were documented, which meant a second contributor, whether human or AI, could not have reproduced my decisions.
When I asked for a blue button, the model wrote #2a60f5 directly into the code. After ten screens I had ten slightly different blues with no way to re-theme any of them.
Tokens kept breaking on the trip from design to build. Figma imports mapped them inconsistently, and I found myself manually fixing the same padding and type drift in every prototype.
// PrimaryButton.jsx background: '#2a60f5', borderRadius: 6, padding: '10px 18px',
// CtaButton.jsx background: '#2f66ff', borderRadius: 8, padding: '12px 22px',
Same button, two files. No shared token — each screen hardcoded its own hex.
To fix this, I named every value in the system for its role instead of its appearance, and then carried that structure through every layer of the workflow: the Figma variables, the Storybook documentation, the Claude skills, and the generated code.
I organized the tokens into three tiers. Foundation tokens like #2A60F5 are raw primitives and never touch components. Brand tokens like Color/Brand/500 exist only to define semantic tokens. Semantic tokens like Colors/button/primary/default are the only tier that components are allowed to use.
One rule governs the whole hierarchy: always pick the token that matches the role, not the appearance. Colors/button/primary/default happens to resolve to #2a60f5 today, but if the brand changes tomorrow, every reference updates in a single change.
| Tier | Example | Used where |
|---|---|---|
| Foundation | #2A60F5 | Raw primitives. Never referenced inside a component. |
| Brand | Color/Brand/500 | Defines semantic tokens only. Never touched directly by a component. |
| Semantic | Colors/button/primary/default | The only tier components touch. |
The system also defines an order of operations. Surfaces are chosen first and content second, so the correct text token always follows from the surface underneath it. State tokens for hover, selected, and disabled bind only to pseudo-states, and a separate accent palette handles category tags so that it stays walled off from the core interface colors.

Colors/button/primary/default (3, semantic) resolves to Color/Brand/500 (2, brand), which resolves to 2A60F5 (1, foundation). A component only ever names the semantic tier.| Figma variable | CSS custom property | Usage rule |
|---|---|---|
| Colors/button/primary/default | --colors-button-primary-default | Primary CTA fill. One per view. |
| Colors/text/on-primary | --colors-text-on-primary | Text on a primary surface. Follows the surface, not the brand. |
| Colors/surface/card | --colors-surface-card | Card and panel background. Chosen before its text token. |
| Colors/button/primary/hover | --colors-button-primary-hover | Binds to :hover only. Never a static fill. |
| Space/space-* | --space-* | The full spacing scale. Every padding and gap steps through it. Never raw px. |
| Radius/radius-* | --radius-* | The full radius scale. Every rounded corner uses a step. Never raw px. |
| Accent/tag/* | --accent-tag-* | The full category-tag palette (violet shown). Category tags only. Walled off from product chrome. |
The token rules solved consistency in color and spacing, but component choice had the same problem: I knew which component fit a situation, but that knowledge lived in my head. So I wrote a selection guide that starts from user intent instead of a catalog, matching what the user is trying to do to one specific component. Narrowing a table calls for a filter chip. Switching between page sections calls for tabs. A persistent warning gets a banner, and a transient confirmation gets a toast.
For the harder calls, I wrote the reasoning down as explicit rules. The dashboard stat tile, for example, is placement-driven and appears only in the top-of-dashboard KPI row, while the same number anywhere else becomes a card with an H3.
I also documented the gaps. Status badges are currently assembled from existing tokens using a transparent fill with a status-colored border and text, and they are flagged as a known gap. The boundary between chips and tabs is still unresolved, so the written rule there is to ask rather than guess.
| Examples of User intent | Reach for | Rule |
|---|---|---|
| Narrow a table to a subset | Filter chip | Narrows one dataset. Does not switch sections. |
| Switch between page sections | Tabs | Changes what is on screen, not what is filtered. |
| Show a headline KPI in the dashboard summary row | Stat tile | Top-of-dashboard KPI row only. |
| Show the same number anywhere else | Card + H3 | Placement, not appearance, decides. |
| Persistent warning that must stay visible | Banner | Stays until dismissed or resolved. |
| Transient confirmation after an action | Toast | Auto-dismisses. Never carries required actions. |
| Status label on a row or card | Badge assembly | Known token gap: outline + status text, no dedicated token yet. |
| Boundary between chip and tab is unclear | Ask | Marked unresolved. Rule is to ask, not guess. |
Here is the same guide applied. Every component in the shipped dashboard below was selected by one of these rules.

The work moved through four sequential passes: audit, tokenization, documentation, and encoding. Each pass produced the input for the next one, so the audit fed the token spec, the spec fed Storybook, and both of them fed the Claude skills.
In this setup, Figma is where the system is designed and Storybook is where it is consumed. Every component page carries its variants, states, props, and the usage rule from the selection guide, which means designers reference Figma variables, developers reference stories and CSS custom properties, and both sides read from the same underlying tokens.

To make the system enforceable, I put together a set of skills Claude Code loads before generating any LendSight UI. I wrote the component-discipline skill from scratch; it checks that both the components and their tokens stay consistent as new screens and components get generated, catching drift a token check alone would not see. The token skill started from one I found online, edited to teach this system's tier rules and block the failure modes I saw in early output: hardcoded hex, brand tokens inside components, hover tokens applied as static fills. The accessibility skill and the code-quality review skill are both adapted from public skills: the accessibility skill enforces the contrast and state rules described below, and the code-quality skill checks the generated output against all of these before it ships.
All four skills enforce traceability. Every generated style names its token; every component names its catalog entry.
## Rule: pick by role, not appearance
Never write a hex value. Resolve every colour to a semantic token.
Semantic tokens are the ONLY tier a component may reference.
# Common mistakes → corrections
✗ background: #2a60f5
✓ background: var(--colors-button-primary-default)
✗ Color/Brand/500 (brand tier — never in a component)
✓ Colors/button/primary/default
✗ hover token used as a static fill
✓ hover binds to :hover only
Writing the skills turned out to be a useful test of the system itself. Whenever a rule was too vague for a model to follow, it usually meant the rule had never been properly defined in the first place.
The same button, generated two ways. Without the skills, raw hex and no trace of intent. With them, CSS variables, token comments, and a component annotation that points back to the catalog.
<button style={{
background: '#2a60f5',
borderRadius: 8,
padding: '10px 16px'
}}>
Run model
</button>
{/* Component: Button / primary → catalog §2.1 */}
<button className="btn-primary">
/* background: var(--colors-button-primary-default)
radius: var(--radius-md)
padding: var(--space-3) var(--space-4) */
Run model
</button>
Each build cycle ran the same loop. I designed in Figma against the token variables, pulled the frames through Figma MCP, generated React with the skills loaded, and reviewed the result against Storybook. Every cycle was a test of whether the rules held up under a builder that follows instructions literally.
Review got faster over time, because every mistake had a name and a rule behind it. A raw hex value broke a token rule. A stat tile on a detail page broke a placement rule. Instead of debating whether something looked right, I could just point to the rule it violated.
Figma
Storybook
| Prop | Type | Default |
|---|---|---|
| state | enum | default |
| active | boolean | false |
| disabled | boolean | false |
Accessibility is built into the token structure itself rather than checked at the end. Surface and text tokens are paired by role, so a failing combination like dark body text on a primary button fill isn't something a screen has to be caught doing, it's something the tokens prevent. Hover, focus, and disabled states are tokens too, so generated components inherit correct behavior instead of inventing their own.
Type roles help too: helper text and metadata have a defined floor of 12px medium, so nothing dips below it ad hoc.
| Surface token | Approved text token | Pairs |
|---|---|---|
| button/primary | text/on-primary | ✓ |
| button/primary | text/body | ✗ fails contrast |
| surface/card | text/body | ✓ |
| surface/card | text/muted | ✓ (≥ 12px only) |
| surface/subtle | text/body | ✓ |
To check this against real standards, I run a public accessibility skill that tests WCAG 2.2 requirements across contrast, keyboard navigation, and screen reader support. The honest limit: the system has not been through a formal WCAG audit yet. Contrast pairs and focus behavior are enforced by rule and checked by the skill, not yet verified by a full audit. That audit is the first item on the roadmap; the skill closes a lot of the gap in the meantime.
Some parts of the system stayed intentionally loose, like chips versus tabs, because forcing a premature decision would have been worse than leaving it open. I'm working on closing these as inconsistencies surface and the use cases become clearer.
The system hasn't been through a formal accessibility audit yet. The skill helps, but it isn't a substitute for one, and a full audit is on the roadmap.
A design system built for an AI-assisted workflow has different requirements than one built for humans, since people fix inconsistency without being told and models don't. That changed how I wrote and thought about instructing AI. I stopped writing rules the way I'd explain something to a teammate, and started writing them the way I'd explain something to someone who takes everything at face value, since every rule would be followed literally, not interpreted. That meant fewer implicit assumptions and more explicit edge cases, even the ones that felt too obvious to write down at first.
There are gaps in this system I know about, and there are probably ones I haven't found yet. I look forward to working on it to bring it to a state where it is most productive and does what it is intended to.