Neem Design System

A single source of truth for all visual tokens. Every color, spacing value, shape, typography style, and motion curve is defined once in tokens.json and consumed through a three-layer CSS custom property cascade: Palette → Theme → Alias. Components only ever reference Layer 3 aliases.

Architecture — Three-Layer Token Cascade

The system prevents hardcoded values by enforcing a strict resolution chain. Components never touch palette or theme layers directly.

┌─────────────────────────────────────────────────────────────────────────┐
│  tokens.json  (single source of truth)                                  │
└────────┬────────────────────────────────────────────────────────────────┘
         │ generates
         ▼
┌────────────────────┐    ┌──────────────────────┐    ┌──────────────────┐
│  Layer 1: Palette  │───▶│  Layer 2: Themes      │───▶│  Layer 3: Alias  │
│  palette.css       │    │  themes.css            │    │  aliases.css     │
│                    │    │                        │    │                  │
│  Raw hex values    │    │  Brand decisions       │    │  Semantic names  │
│  --neem-palette-   │    │  --neem-primary        │    │  --neem-surface- │
│    cyan20: #6CD2FF │    │    : var(--neem-       │    │    primary: var( │
│                    │    │      palette-cyan20)   │    │    --neem-primary│
│  Defined once.     │    │  One block per theme.  │    │  )              │
│  Never referenced  │    │  Adding a new theme =  │    │  Components ONLY│
│  by components.    │    │  adding one block.     │    │  read these.    │
└────────────────────┘    └──────────────────────┘    └──────────────────┘

 Theme switching = changing data-neem-theme attribute on <body>
 Layer 2 resolves differently → Layer 3 aliases auto-update → Components re-paint

How to load

<!-- Option A: Single bundle import -->
<link rel="stylesheet" href="neem/theme.css" />

<!-- theme.css contains: -->
@import "./palette.css";   /* Layer 1 */
@import "./themes.css";    /* Layer 2 */
@import "./aliases.css";   /* Layer 3 */

<!-- Then load component stylesheets -->
<link rel="stylesheet" href="neem/components/button.css" />

<!-- Set the active theme -->
<body data-neem-theme="blue">  <!-- or "green" / "dark" -->

tokens.json — Single source of truth

All tokens originate from this JSON file. CSS files, Ruby helpers, and any future platform (iOS, Android) read from it.

{
  "palette": {
    "cyan20": "#6CD2FF",
    "green20": "#006C45",
    "navy30": "#19355E",
    ...
  },
  "spacing": {
    "extraExtraSmall": "4px",
    "extraSmall": "8px",
    "small": "16px",
    "medium": "24px",
    "large": "32px",
    "extraLarge": "48px",
    "extraExtraLarge": "64px"
  },
  "themes": {
    "blue":  { "surface": { "primary": "cyan20",  "onPrimary": "cyan50"    } },
    "green": { "surface": { "primary": "green20", "onPrimary": "neutral10" } },
    "dark":  { "surface": { "primary": "darkPrimary", "onPrimary": "darkOnPrimary" } }
  }
}

Layer 1 — Palette

Raw color values defined once. Never referenced by components directly. If a color changes, update it here only.

Cyan

cyan10#BFE9FF
cyan20#6CD2FF
cyan30#187BC2
cyan40#004D65
cyan50#003547

Green

green5#EDFFF3
green10#D4FAE0
green20#006C45
green30#124854

Navy

navy10#EAF9FF
navy20#186D92
navy30#19355E

Red / Error

red10#FFDAD6
red20#BA1A1A
red30#410002
red40#FF897D

Yellow

yellow10#FFF6C8
yellow30#5F4100

Neutral

neutral10#FFFFFF
neutral20#F7F7F7
neutral30#E5E5E5
neutral40#D9D9D9
neutral50#999999
neutral60#666666
neutral70#333333

Dark Mode

darkSurface#121212
darkSurfaceVariant#1E1E1E
darkOnSurface#E1E1E1
darkPrimary#6CD2FF
darkOnPrimary#003547

Code — palette.css

:root {
  --neem-palette-cyan10: #BFE9FF;
  --neem-palette-cyan20: #6CD2FF;
  --neem-palette-cyan30: #187BC2;
  --neem-palette-cyan40: #004D65;
  --neem-palette-cyan50: #003547;

  --neem-palette-green5:  #EDFFF3;
  --neem-palette-green10: #D4FAE0;
  --neem-palette-green20: #006C45;
  --neem-palette-green30: #124854;

  --neem-palette-navy10: #EAF9FF;
  --neem-palette-navy20: #186D92;
  --neem-palette-navy30: #19355E;

  --neem-palette-red10: #FFDAD6;
  --neem-palette-red20: #BA1A1A;
  --neem-palette-red30: #410002;

  --neem-palette-neutral10: #FFFFFF;
  --neem-palette-neutral20: #F7F7F7;
  --neem-palette-neutral30: #E5E5E5;
  /* ... */
}

Layer 2 — Themes

Each theme maps the same semantic token names to different palette values. Adding a new theme = adding one CSS block. No aliases or components change.

Live Theme Aliases (changes with theme toggle above)

--neem-primary
--neem-on-primary
--neem-primary-container
--neem-secondary
--neem-on-secondary
--neem-secondary-container
--neem-tertiary
--neem-error
--neem-surface
--neem-on-surface
--neem-surface-container
--neem-surface-container-high

Code — themes.css

/* Blue (default) */
[data-neem-theme="blue"], :root {
  --neem-primary:              var(--neem-palette-cyan20);
  --neem-on-primary:           var(--neem-palette-cyan50);
  --neem-secondary:            var(--neem-palette-navy30);
  --neem-on-secondary:         var(--neem-palette-neutral10);
  --neem-surface:              var(--neem-palette-neutral10);
  --neem-on-surface:           var(--neem-palette-neutral70);
  --neem-shadow:               var(--neem-palette-shadow-light);
}

/* Green — only the brand colors differ */
[data-neem-theme="green"] {
  --neem-primary:              var(--neem-palette-green20);
  --neem-on-primary:           var(--neem-palette-neutral10);
  --neem-secondary-container:  var(--neem-palette-green5);
}

/* Dark — surfaces and text flip for dark backgrounds */
[data-neem-theme="dark"] {
  --neem-primary:              var(--neem-palette-dark-primary);
  --neem-on-primary:           var(--neem-palette-dark-on-primary);
  --neem-surface:              var(--neem-palette-dark-surface);
  --neem-on-surface:           var(--neem-palette-dark-on-surface);
  --neem-shadow:               var(--neem-palette-shadow-dark);
}

Layer 3 — Semantic Aliases

Components ONLY reference these tokens. They never change between themes — the cascade handles resolution automatically.

Surface Aliases

--neem-surface-default
--neem-surface-primary
--neem-surface-on-primary
--neem-surface-secondary
--neem-surface-secondary-container
--neem-surface-tertiary
--neem-surface-error
--neem-surface-disabled-container
--neem-surface-variant

Text Aliases

--neem-text-on-surface
--neem-text-primary
--neem-text-on-primary
--neem-text-secondary
--neem-text-error
--neem-text-disabled
--neem-text-link

Border Aliases

--neem-border-secondary
--neem-border-disabled
--neem-border-on-surface
--neem-border-active

Icon Aliases

--neem-icon-primary
--neem-icon-secondary
--neem-icon-success
--neem-icon-error
--neem-icon-disabled

Code — aliases.css (excerpt)

:root {
  /* Surface */
  --neem-surface-primary:            var(--neem-primary);
  --neem-surface-disabled-container: var(--neem-surface-container-high);
  --neem-surface-error:              var(--neem-error);

  /* Text */
  --neem-text-on-primary:            var(--neem-on-primary);
  --neem-text-secondary:             var(--neem-secondary);
  --neem-text-disabled:              var(--neem-surface-container-highest);

  /* Border */
  --neem-border-secondary:           var(--neem-secondary);
  --neem-border-disabled:            var(--neem-surface-container-high);

  /* Icon */
  --neem-icon-success:               var(--neem-palette-green20); /* always green */
}

Typography

All type styles are tokenized. Components reference --neem-typography-* variables — never raw px values.

display.small The quick brown fox 36px / 48px
headline.medium The quick brown fox 28px / 40px
headline.small The quick brown fox 20px / 32px
title.large The quick brown fox 20px / 32px
title.medium The quick brown fox 16px / 24px
body.medium The quick brown fox jumps over the lazy dog 14px / 20px
body.small The quick brown fox jumps over the lazy dog 12px / 16px
label.large (bold) CONFIRM PAYMENT 16px / 24px / 0.8px
label.medium Label Medium 14px / 24px / 0.7px
label.small Label Small 12px / 16px / 0.6px
code const x = tokens.json; 13px / 1.5

Font Weights

weights.normal Normal weight text (400) 400
weights.bold Bold weight text (700) 700

Code — Typography tokens

:root {
  --neem-typography-display-small-size:         36px;
  --neem-typography-display-small-line-height:  48px;
  --neem-typography-headline-medium-size:       28px;
  --neem-typography-headline-medium-line-height:40px;
  --neem-typography-headline-small-size:        20px;
  --neem-typography-body-medium-size:           14px;
  --neem-typography-body-small-size:            12px;
  --neem-typography-label-large-size:           16px;
  --neem-typography-label-large-letter-spacing: 0.8px;
  --neem-typography-code-size:                  13px;
  --neem-typography-weights-normal:             400;
  --neem-typography-weights-bold:               700;
}

Spacing

A linear spacing scale used for padding, margins, gaps, and component dimensions. Mirrors Kotlin NeemSpacing.

TokenValueVisual
--neem-spacing-none0px
--neem-spacing-border1px
--neem-spacing-extra-extra-small4px
--neem-spacing-extra-small8px
--neem-spacing-small16px
--neem-spacing-medium24px
--neem-spacing-large32px
--neem-spacing-extra-large48px
--neem-spacing-extra-extra-large64px
--neem-spacing-extra-extra-extra-large80px

Shapes (Border Radius)

Mirrors Kotlin NeemShapes. Used for buttons, cards, inputs, badges.

TokenValueVisual
--neem-shapes-extra-small2px
--neem-shapes-small4px
--neem-shapes-medium8px
--neem-shapes-large16px
--neem-shapes-extra-large32px
--neem-shapes-full9999px

Border Width

Two border widths for standard and emphasized borders.

TokenValueVisual
--neem-border-width-thin1px
--neem-border-width-bold2px

Motion

Duration and easing tokens for consistent animations across all components.

TokenValue
--neem-motion-duration-short150ms
--neem-motion-duration-medium300ms
--neem-motion-duration-long500ms
--neem-motion-easing-standardcubic-bezier(0.4, 0, 0.2, 1)

Live Motion Demo

short (150ms)
medium (300ms)
long (500ms)

Component — Button

The button component exists across all frameworks. Every visual value comes from tokens. Toggle the theme above to see all states update instantly.

All States

Primary — Default
Secondary — Default
Primary — Loading
Secondary — Loading
Primary — Disabled
Secondary — Disabled
Primary — With Leading Icon
Secondary — With Trailing Icon

Token Mapping — What the button reads

/* Every button property maps to a Layer 3 alias token */

.neem-button {
  height:         var(--neem-spacing-extra-extra-large);         /* 64px  */
  padding:        0 var(--neem-spacing-medium);                  /* 24px  */
  border-radius:  var(--neem-shapes-large);                      /* 16px  */
  font-size:      var(--neem-typography-label-large-size);       /* 16px  */
  font-weight:    var(--neem-typography-weights-bold);            /* 700   */
  letter-spacing: var(--neem-typography-label-large-letter-spacing); /* 0.8px */
  transition:     all var(--neem-motion-duration-short)           /* 150ms */
                      var(--neem-motion-easing-standard);
}

.neem-button--primary {
  background: var(--neem-surface-primary);   /* cyan20 → green20 → darkPrimary */
  color:      var(--neem-text-on-primary);   /* cyan50 → neutral10 → darkOnPrimary */
}

.neem-button--secondary {
  border: var(--neem-border-width-bold) solid var(--neem-border-secondary);
  color:  var(--neem-text-secondary);
}

.neem-button:disabled {
  background:   var(--neem-surface-disabled-container);
  color:        var(--neem-text-disabled);
  border-color: var(--neem-border-disabled);
}

Framework Integrations

The same token system works identically across every framework. See each demo page for full implementations.

Available Implementations

FrameworkApproachDemo
Lit 3Shadow DOM custom element, CSS vars pierce boundaryView Demo
BEM (ViewComponent)Plain <button> + global CSS classes, zero JSView Demo
Hybrid (VC + Lit)Ruby renders <neem-button>, Lit hydrates client-sideView Demo
Tailwind + ViewComponentUtility classes with arbitrary token referencesView Demo
React 19Functional component + BEM classes + shared button.cssView Demo
Vue 3Composition API SFC, no <style scoped> neededView Demo

Principle — Framework swappability

Because every framework reads from the same token cascade, you can swap the internal rendering engine (Lit → React, BEM → Tailwind) without affecting the design. The contract is the CSS custom properties, not the framework.

/* This is the contract. Every framework reads these same variables. */
/* Change the framework? The variables don't change. */
/* Change the theme? The framework doesn't change.   */

Component CSS  →  reads  →  Layer 3 Aliases  →  resolves via  →  Layer 2 Theme  →  Layer 1 Palette
                            (--neem-surface-*)                   ([data-neem-theme])    (--neem-palette-*)
                            Never changes.                       Swappable.             Defined once.