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.

Role
Founding Product Designer
Timeline
Aug 2025 – Present
Skills
Design tokens, Figma variables, shadcn, AI-assisted UI
Team
Founder, PM, engineering

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.”

Sambit Nayak