Skip to content

All work

Aether Design System

About 250 components, every state documented, and lint warnings cut from 1,594 to 0 by turning the standard into tooling.

Role
Co-founder; UI, design and frontend lead
Team
2
Timeline
2025 - 2026
Tools
UI DesignFigmaReactTypeScriptTailwind CSSStorybookESLintClaude Code

Problem

When I joined, there was no variable or token system at all. Colours, type and spacing were set by hand, component by component. With two people shipping two products, every one-off choice became something to fix later.

AI made the problem sharper. In long Claude Code sessions, the standard drifted no matter how often I restated it, started fresh chats or compacted context. Claude would leave filler comments that meant nothing after the session, hardcode Tailwind classes and arbitrary opacity values instead of tokens, create primitive-like helpers outside the primitives folder, and drop our naming and file-layout conventions. Written rules weren’t enough. The standard had to enforce itself.

Key decisions

I. Build the tokens in layers, one step at a time

Each colour as a 50 to 900 scale. Components never read a step directly.

The system grew in stages: surface naming first, then 100–900 scales, then neutral families mapped to each brand colour, then role tokens such as surface, on-surface and stroke. Components may only use roles, never a raw colour, so light and dark mode work everywhere by default and a palette change never touches component code.

Neutrals do most of the work. Surfaces, text, strokes, overlays and fills all read from a neutral ramp, so the brand colour is kept for the moments that need it. Each brand colour has its own neutral family, tinted toward it and given its own background step, so the greys sit with the brand instead of reading as a default grey, and changing the brand carries the whole interface with it.

Components read roles such as surface and on-surface, never a raw palette step.

II. Let primitives own the libraries

Only primitives touch Base UI and Ark UI; everything above builds on them.

Only primitives may import Base UI or Ark UI. Composites are built from primitives alone. A library change stays contained in one layer, and every dropdown, dialog and field shares the same focus, positioning and accessibility behaviour.

III. Show every component in every state

Every component change ships with its Storybook stories, run against a mocked API that can load, fail and time out on demand. A skeleton primitive gives loading states one consistent look.

IV. Turn the standard into lint

I turned the standard into 4 custom ESLint rules: tokens, typography, constant naming and class reuse. I stated the rules, patterns and syntax; Claude wrote them and applied them. A hook runs them on every file Claude edits and hands errors back immediately, and CI runs the same rules as a backstop. A session can forget a written rule, but a lint error comes back at the moment the rule is broken, so the standard no longer depends on what the session remembers.

Result

The system now carries the whole product: tokens, surface and on-surface ramps, neutral families, cards, dialogs and a Text primitive that is the only place type styles are defined. I designed every component in it, mocking them up in Figma at first. The system is now robust enough that I build most new work directly in code, and I keep Figma for wireframes when pitching a new feature.

after turning the design system standards into lint rules
1,594 lint warnings to 0
in Aether's token system
~250components
in Storybook covering every state
~1,100stories

What I’d do differently

I over-built early. Ethos, a shared theming package with three switchable archetypes, was a system I loved, but at the go-to-market stage every token change meant republishing a package. I cut it back so the product could own its tokens directly. Next time, I’d build the flexible version only once a second product actually needs it.