All work
role
Design systems lead, and the person who builds it
system
BELLA, my own open design system
scope
Token tiers, themes, contrast gate, Figma ⇄ code
proof
Light and dark live on this site; axe runs clean in both
token strategy figma → codeconsistency without fragmentationaccessibility in every themeai-ready structure
tier 1 · primitives
tier 2 · semantic
tier 3 · component
the gate · every theme must pass before it ships
body text0.00AAA
muted text0.00AAA
button label0.00AAA
input border0.00≥ 3:1
bel·la homesCanet de Mar · any week · 2 guests
new
Canet de Mar, Spain★ 4.92
2 rooms · 64 m² · 5 min to the beach
€1,150 monthBook a visit
themegroundBELLA · light
light mode{color.light.bg}#ffffff
the component asks forbackground
dark mode{color.dark.bg}#0d0d0d

and here it is in the real Storybook table ↓

Storybook · Foundations / Colors / Semantic
Semantic tokens, light theme: background points to color.light.bg
light · semantic/light.json
Semantic tokens, dark theme: background points to color.dark.bg
dark · semantic/dark.json
the idea

Components only read meaning. A button reads action, never blue-600.

what shows it

Four themes above, one component tree. Switching theme swaps nine semantic pointers and touches zero components.

why it matters

A new brand or market is a token file, not a redesign. And it can't ship below the contrast bar.

// tier 2 · ground.tokens.json · the same names in every theme
{
  "theme": "ground",
  "semantic": {
    "bg": "{neutral.0}",
    "panel": "{neutral.1}",
    "line": "{neutral.2}",
    "muted": "{neutral.5}",
    "ink": "{neutral.7}",
    "action": "{neutral.7}",
    "on-action": "{neutral.0}",
    "accent": "{brand.3}"
  }
}
emcdanie/bella · tokens/semantic/light.json → dark.json

In plain words: each name on the left has one answer for light mode and one for dark.

"text-primary"   {color.light.ink}      {color.dark.ink}
"text-muted"     {color.light.muted}    {color.dark.muted}
"surface-card"   {color.light.panel}    {color.dark.surface}
"border-strong"  {color.light.control}  {color.dark.control}
"focus-ring"     {color.brand.ochre-deep} {color.brand.ochre}
"chip-1"         {color.chip.c1}        {color.chip.c1}   // same in both
emcdanie/bella · tokens/component.json

In plain words: the Button's rulebook. Which versions exist, which states it has, and how not to use it. People and AI tools both read this.

"button": {
  "name": "Button",
  "variants": ["primary", "secondary", "tertiary"],
  "states": ["default", "hover", "active", "focus", "disabled"],
  "dont": [
    "Do not render more than one primary per view",
    "Do not use for filters, toggles, or sort"
  ]
}
emcdanie/bella · tokens/$themes.json

In plain words: the reading order. Raw values first, then what they're for, then what uses them.

"tokenSetOrder": [
  "primitive",       // tier 1 · raw values
  "semantic/light",  // tier 2 · what it's for
  "semantic/dark",
  "component"        // tier 3 · what reads it
]
writetokens/*.jsonbuildbuild.pygeneratecss · json · themecheckthe gateshipstorybook + siteactionwritetokens/*.jsonbuildbuild.pygeneratecss · json · themecheckthe gateshipstorybook + site
emcdanie/bella · package.json · the gate, in four checks
  1. Rebuild every token output from the source files.

    npm run build
  2. If the rebuild differs from what's committed, stop. Someone edited by hand or forgot to rebuild.

    git diff --exit-code tokens/bella.css tokens/bella.json …
  3. Check the code still matches each component's contract.

    node scripts/contract-parity.mjs
  4. Screenshot every story, compare it, and run accessibility checks.

    npm run audit:visual
bel·la homes
new
Canet de Mar, Spain★ 4.92
2 rooms · 64 m² · 5 min to the beach€1,150 month
ground · BELLA
bel·la homes
new
Canet de Mar, Spain★ 4.92
2 rooms · 64 m² · 5 min to the beach€1,150 month
night · BELLA
bel·la homes
new
Canet de Mar, Spain★ 4.92
2 rooms · 64 m² · 5 min to the beach€1,150 month
coast · demo brand
bel·la homes
new
Canet de Mar, Spain★ 4.92
2 rooms · 64 m² · 5 min to the beach€1,150 month
market · demo brand
  1. Components never read tier 1. A new need gets a new named role first.

  2. Dark is a theme, not an inversion. Its own greys, chosen so muted text still clears 7:1.

  3. Colour lives in fills. Text and lines stay ink or grey, so a brand change can't hurt readability.

  4. Every theme passes the gate. AAA for text, 3:1 for controls, checked on every story before merge.

  5. The same names in Figma and code. Designers and developers say surface.panel and mean the same thing.