Work/CotonAI Design System
Case study ( 03 ) · CotonAI
The canvas stays quiet: a token system that names every gray and keeps one blue.
CotonAI is a fashion board. The system under it names every gray by its lightness, keeps one blue in light and dark, and measures the chrome so it never competes with the work.
Overview
The canvas stays quiet.
CotonAI is a fashion board. The work on it is the product, so the chrome around it has to stay out of the way. The system under it names every gray by its lightness, keeps one blue in both themes, and measures the chrome so it never competes with the work.
I owned the design system as founding product designer, with the founder, PM and engineering.
Problem: Color was a class, not a name.
Hex strings in Tailwind classes, and HSL variables that only covered the shadcn layer.
Solution: Four collections, shaped like Figma variables.
Primitives, semantic aliases, dimensions and effects, with Light and Dark as modes.
Outcome: A file Figma can import.
57 primitives, 48 semantic aliases, 2 modes, regenerated from the stylesheet.
Problem
The board was designed. The names were not.
Board chrome, the inspector and the dashboard were already a coherent picture: white surfaces, a soft gray page, a single blue for anything that means “this.” The picture was not a system. It lived as hex strings inside Tailwind classes, and as HSL variables in index.css that only covered the shadcn layer.
Light and dark were two parallel strings, not two modes of one token. #232323 and #242424 both meant “dark control.” A Figma file had nothing honest to import, because the code had never agreed on a name.
Color was a class
Values lived inside Tailwind strings, not behind names.
Two themes, two strings
Light and dark were parallel values, not modes of one token.
Nothing to import
Figma had no honest source, because the code never agreed on a name.
Collections
Four, shaped like Figma variables.
The extraction reads src/index.css, boardUiTokens.ts and tailwind.config.ts. What repeats becomes a token. What is a role becomes an alias. What is a shadow stays a style, because a variable is the wrong container for a blur.
The four collections
- 01
Primitives
Raw values, 57 in one mode. Grays are named by HSL lightness, the way the CSS was already written. gray/96 is the page.
- 02
Semantic
48 roles aliased to primitives: background, border, text, icon, accent, success, danger. Light and Dark are modes of one collection.
- 03
Dimensions
Spacing, radius, control sizes, icon sizes, type, opacity and duration, in one mode.
- 04
Effects
Four styles: panel, popover and overlay shadows. They become Figma effect styles, not color variables.
Chrome
Measured, then named.
These sizes were not invented for a scale. They are the measurements the board already used, written down so the next surface can match them.
Inset: 17px
Space between the board and the screen edge
Top bar: 45px
Chrome buttons are 28px with a 12px radius
Inspector: 300px
Panel width
Floating asset bar: 38px
Buttons 26px, icons 14px, radius 10px
Default UI type: 12px
Inter. Focus field is a 30% blue fill with a full blue stroke.
Color
Grays by lightness, and one blue that does not flip.
A primitive is a fact. gray/96 is always #F5F5F5. A semantic token is a job. In light mode that job is the page; in dark mode the page is gray/2.
Accent, Generate and Cancel keep their hue in both modes. Accent is #0C8CE9 in light and dark, with #0A7BD0 on hover and #096BB8 on press.
Decision 1: Name grays by lightness
Issue
Hex values repeated across files with no shared name.
Change
Primitives named by HSL lightness, as the CSS already did. gray/96 is #F5F5F5, gray/2 is #050505.
Result
A primitive is a fact and a semantic token is a job, so Light and Dark become two modes of one collection.
Decision 2: One blue in both themes
Issue
Accent had to survive a switch from a white page to a near-black one.
Change
#0C8CE9 stays in both modes. Generate and Cancel keep their hue too.
Result
Only the paper changes, from gray/96 to gray/2. That is the whole semantic model.
Decision 3: Two layers, one blue and one gray page
Board chrome
boardUiTokens.ts: New project, Credits, Generate and Cancel.
App UI
button.tsx: six shadcn variants, default, destructive, outline, secondary, ghost and link, with one radius.
Shared
Both layers use the same blue and the same gray page. Control sizes are 40, 36, 44 and a 40px icon square.
Blue points
Focus, selection and the primary action. A pointer, not a personality.
Green commits
Generate is #71CE82 as type until hover, then a 30% fill and a full stroke.
Red stops
Cancel borrows the same ghost logic in #FF383C. Press deepens the fill to 50%.
Exclusions
Near-duplicates collapsed, debug color stayed out.
Three dark grays were close enough to be the same decision. They now point at one primitive each: #232323 and #242424 to gray/14, #1C1C1C and #1D1D1D to gray/11, #292929 and #2A2A2A to gray/16.
Colors that only appeared as debug marks were not given a name: #FF0000, #00FF00, #3B82F6, #6366F1, #3B6BFF and #FFF44F. #8A38F5 stayed, but only as icon/layer-text, the text-layer mark in the layers panel. It is a job, not a brand color.
Two line heights, 14 for 10px type and 18 for 13px type, are proposed. The code never set them.
Handoff
From the stylesheet to a file Figma can import.
design-tokens/build.py regenerates the token files. The W3C files in design-tokens/figma are one collection per file. cotonai.tokens.json is the same system in a single file for Tokens Studio.
From the stylesheet to Figma
- src/index.css
- boardUiTokens.ts
- tailwind.config.ts
- Figma-importable tokens
design-tokens/build.py regenerates the files.
Import order
- 01
primitives.tokens.json
Imported first.
- 02
semantic.light and semantic.dark
Imported as two modes of one collection.
- 03
dimensions and effects
Dimensions as variables, effects as styles.
Impact
One named system, in two modes.
The code and Figma now agree on a name for every color the board uses. The tokens are regenerated from the stylesheet, so a new surface matches the measurements the board already had.
Primitives: 57
Named values, one mode
Semantic aliases: 48
Light and Dark as modes of one collection
Accent: 1 blue
#0C8CE9 in light and dark
Learning
A variable is the wrong container for a blur.
Naming the grays was the easy part. The work was deciding what not to name: three near-duplicate darks collapsed into one primitive each, and debug colors stayed out. A system is as much what it refuses as what it holds.
The limit: the line heights are proposed, not measured, and the tokens only stay honest while build.py keeps regenerating them from the stylesheet.
“The board was designed. The names were not.”