Skip to content

dev-launchers/design-system

Repository files navigation

Universal Design System — Token Pipeline

A CSS custom property design token system with a layered resolution pipeline, React component library, and Storybook documentation.


Quick Start

yarn install
yarn storybook       # dev server at localhost:6006
yarn dev             # Vite dev app
yarn build-storybook # static Storybook build

Token Pipeline Architecture

Tokens are loaded in strict dependency order via css/index.css. Each layer builds on the one below it — never skip layers or import out of order.

01-primitives      Raw hex values and unitless numbers. No references.
02-theme           Light / dark palette direction. Sets --color-* scale.
03-sub-styles      Family selectors: eclipse, onyx, echo, nebula, cosmic, etc.
04-style           Resolves --style-* unified scale (---style-00 → ---style-12).
05-priority-toggle Maps interaction states (default/hover/focus/active) to style steps.
06-status          Activates the correct state via CSS pseudo-classes or data-status.
07-semantic        Component-ready names: --surface-priority, --border-priority, etc.
── spacing         Gap, padding, border-radius tokens.
── typography      Font family, size, weight.
── size            Component dimension tokens (--size-button-y, --size-button-x, etc.)
── device-grid     Responsive breakpoints.
── flags           Boolean state tokens.

Two Access Patterns

Interactive (buttons, toggles, inputs):

theme → substyle → style → priority-toggle → status → semantic

Consumed as var(--surface-priority), var(--border-priority), var(--content-priority).

Non-interactive (static surfaces, text, dividers):

theme → substyle → style → semantic

Consumed as var(--surface-01), var(--content-02), etc.


Data Attributes

Every component sets these on its root element to wire the token chain.

Always set (both interactive and non-interactive)

Attribute Values Notes
data-theme light | dark Omit to inherit from parent
data-semantic theme | inverse Always "theme" unless explicitly inverting
data-style cta | grey | alert | brand Selects the color family

Paired substyle attribute

data-style Substyle attribute Valid values
cta data-cta nebula | cosmic
grey data-grey eclipse | onyx | echo
alert data-alert error | info | warning | success
brand data-brand jupiter | uranus | neptune | saturn

Interactive only

Attribute Values Notes
data-priority primary | secondary | tertiary Selects which style steps map to each state
data-toggle selected | unselected Alternative to priority for chip/toggle components
data-interactive "priority" | "toggle" Required marker for pseudo-class CSS rules
data-size component-specific Drives dimension tokens
data-status default | hover | focus | active | disabled Storybook forced states only

In production, data-status is never set. The :hover, :focus-visible, :active, and :disabled pseudo-classes in 06-status.css handle all state changes automatically.


Interaction Ring System

Interactive components use a 2 + 2 + 2 inset box-shadow ring to communicate state without any extra DOM elements. The Figma "interaction wrapper" frame maps directly to this CSS pattern.

box-shadow:
  inset 0 0 0 var(--priority-ring-surface-spread) var(--priority-ring-border),
  inset 0 0 0 var(--priority-ring-border-spread)  var(--priority-ring-surface);

Visual stack (outside → in)

CSS border (2px, --border-priority)          ← outermost, rendered on top of inset shadows
inset layer 1 (2px, ring-border color)       ← halo band just inside the border
inset layer 2 (4px spread, ring-surface)     ← ring accent; only the 2–4px band is visible
button fill

Spread values by state (06-status.css)

State ring-surface-spread ring-border-spread Visible rings
default 0 0 None
hover 2px 0 Border + halo
focus 2px 4px Border + halo + ring
active 2px 4px Border + halo + ring
disabled 0 0 None (opacity 0.4)

Focus uses :focus-visible, not :focus

Focus rings only appear on keyboard/assistive navigation, not mouse clicks. This is handled entirely in CSS — no JavaScript required.

Important: ring colors must be valid CSS colors

--focus-ring-border must resolve to a real color value. The --num-0 primitive (0, a unitless number) is invalid as a color — using it causes the entire box-shadow declaration to be silently discarded. Use transparent or a style-scale value.


interactiveAttrs Helper

components/interactiveAttrs.ts is the single source of truth for assembling the data-* attribute set for any interactive component. Spread its return value directly onto a native element.

import { interactiveAttrs } from './interactiveAttrs';

<button
  {...interactiveAttrs({
    colorStyle: 'cta',
    substyle:   'nebula',
    priority:   'primary',
    theme:      'dark',      // omit to inherit
    semantic:   'theme',
    status:     status,      // Storybook only — undefined in production
    size:       'base',
  })}
  disabled={disabled}
/>

All future interactive components should call this helper rather than assembling data-* attributes manually.


CSS Tokens Reference

Interactive (priority pipeline)

background:   var(--surface-priority);
border-color: var(--border-priority);
color:        var(--content-priority);
opacity:      var(--component-opacity);   /* 1 = normal, 0.4 = disabled */

Non-interactive (fixed level)

background:   var(--surface-01);   /* 00 darkest → 04 most elevated */
border-color: var(--border-01);    /* 00 strongest → 04 subtlest */
color:        var(--content-01);   /* 00 strongest → 04 subtlest */

Spacing

gap: var(--gap-xs);   /* 4px */
gap: var(--gap-sm);   /* 8px */
gap: var(--gap-md);   /* 12px */
gap: var(--gap-lg);   /* 16px */

Typography

font-family: var(--text-family-manual-secondary, "Nunito Sans", sans-serif);
font-size:   var(--text-size-body);

Note: --text-weight-mode-weight resolves to Figma variable font axis strings ("Normal", "SemiBold"), not valid CSS font-weight values. Use numeric weights (400, 600) instead.


Component Checklist

Interactive component

<element
  {...interactiveAttrs({ colorStyle, substyle, priority, theme, semantic, status, size })}
  disabled={disabled}
  style={{
    background:  'var(--surface-priority)',
    border:      'var(--priority-border-width) solid var(--border-priority)',
    color:       'var(--content-priority)',
    opacity:     'var(--component-opacity)',
    boxShadow: [
      'inset 0 0 0 var(--priority-ring-surface-spread) var(--priority-ring-border)',
      'inset 0 0 0 var(--priority-ring-border-spread)  var(--priority-ring-surface)',
    ].join(', '),
  }}
/>

Non-interactive component

<element
  data-theme={theme}
  data-semantic="theme"
  data-style="grey"
  data-grey="eclipse"
  style={{ background: 'var(--surface-01)', color: 'var(--content-01)' }}
/>

Storybook

Stories live in storybook/components/. Each component story exports:

  • Playground — fully controllable via the Controls panel. Substyle options update automatically when colorStyle changes (conditional controls via argTypes.if).
  • States — forced states via data-status for visual inspection.
  • Interaction Ring — side-by-side light/dark view showing all five states across all three priorities, demonstrating the inset ring system across surfaces.
  • Priorities / Sizes / Icons / CTA / Grey / Alert — static reference grids.

Theming

data-theme is set on <html> by @storybook/addon-themes via withThemeByDataAttribute. The toolbar switcher toggles between Dark (default) and Light.

Forced states vs live interactions

Method When to use
data-status="focus" on element Storybook stories — pins a state for inspection
CSS :focus-visible / :hover / :active Production — all real interactions, no JS needed

TypeScript Tokens Reference (tokens.ts)

tokens.ts exports constants for every CSS variable name so you never write a magic string.

import { surface, border, content, text, gap, rounded, borderWidth, dataAttr, token } from './tokens';
import type { Priority, Toggle, ColorStyle, Theme, Semantic, Size } from './tokens';

// Wrap a token name for inline styles
style={{
  background:   token(surface[1]),   // 'var(--surface-01)'
  color:        token(content[2]),   // 'var(--content-02)'
  gap:          token(gap.md),       // 'var(--gap-md)'
  borderRadius: token(rounded.sm),   // 'var(--rounded-sm)'
}}

// Spread data attributes type-safely
<div {...dataAttr.theme('dark')} {...dataAttr.semantic('inverse')}>

// Constrain prop types
function MyButton({ priority }: { priority: Priority }) {  }
function MyChip({ toggle }: { toggle: Toggle }) {  }

Token namespaces

Export CSS variables covered
surface --surface-00…04, --surface-priority, --surface-toggle
border --border-00…04, --border-priority, --border-toggle
content --content-00…04, --content-priority, --content-toggle
base --base-00…04, --base-priority, --base-toggle
interactive --interactive-surface/border/content/border-width
priorityRing --priority-ring-border/surface/border-spread/surface-spread
overlay --overlay-muted
coreColors --semantic-core-jupiter/saturn/neptune/uranus/cosmic/nebula
borderWidth --border-width-none/2xs/xs/sm/sm-base/base/md-base/md/lg/xl/2xl/3xl
rounded --rounded-full/none/3xs/2xs/xs/sm/base/md/lg/xl/2xl/3xl
padding --padding-none/3xs/2xs/xs/sm/base-sm/base/…/8xl
gap --gap-0/2xs/xs/sm/md/lg/xl/2xl/…/9xl
text.size --text-size-3xs … --text-size-7xl
text.family manual font-family vars
iconSize --size-icon-3xs … --size-icon-4xl
componentSize --size-avatar, --size-button-text/icon/x/y, --size-tags-*, --size-pill-*
opacity --component-opacity

dataAttr helper

dataAttr.theme('dark')          // → { 'data-theme': 'dark' }
dataAttr.priority('primary')    // → { 'data-priority': 'primary' }
dataAttr.toggle('selected')     // → { 'data-toggle': 'selected' }
dataAttr.size('lg')             // → { 'data-size': 'lg' }
dataAttr.disabled(true)         // → { 'data-disabled': 'true' }
dataAttr.expanded(false)        // → { 'data-expanded': 'false' }

Component Index (components/index.ts)

All components re-exported from a single barrel file:

// Default component imports
import Button        from './components/Button';
import { default as Table, type TableColumn } from './components/Table';

// Or via the barrel
import { Button, Table, type TableColumn, type TokenOptions } from './components';

TokenOptions is exported once from the barrel (from interactiveAttrs) — import it from here rather than from individual component files.

Fully implemented (reference)

Export Description
Icon Phosphor icon wrapper — sizes via --size-icon-*
Button Full priority pipeline — copy this pattern for other interactive components
Card Semantic-surface layout panel

Typed stubs (implement per TODO comment in each file)

Identity / media: Avatar, AvatarBlock, AvatarGroup, Logo

Labels: Badge, BadgeNotification, Tag, Pill

Navigation: Link, Navigation, Sidebar, Tabs, Pagination

Forms: Checkbox, RadioButton, Switch, Slider, InteractionButton, TextInput, TextArea, SearchBar, DatePicker, Upload

Selection: SingleSelect, Multiselect

Data display: Table, Lists, Accordion, ProgressBar

Feedback / overlays: Toast, Notifications, Dialog, Popover, Tooltip

Motion: Carousel

Layout: Scrollbar, ScrollArea, Separator, Footer, Sections, PageLayout, HomePage, DashboardPage

Named type exports

import type {
  TokenOptions,       // interactiveAttrs input shape
  AccordionItem,      // { id, label, content, disabled? }
  ListItem,           // { id, label, meta?, icon?, disabled? }
  NavigationItem,     // { id, label, href?, icon?, disabled? }
  NotificationItem,   // { id, title, message?, timestamp?, read?, actions? }
  SelectOption,       // { value, label, disabled? }
  SidebarItem,        // { id, label, href?, icon?, disabled?, children? }
  TabItem,            // { id, label, content, disabled? }
  TableColumn,        // { key, header, accessor, sortable?, width? }
} from './components';

Implementing a Stub Component

Each stub file contains everything needed except the JSX. The pattern:

  1. Read the TODO comment — it lists exactly which pipeline and tokens to consume.
  2. Replace return null with your JSX.
  3. For interactive components, spread interactiveAttrs(props) on the interactive element.
  4. Reference --interactive-* tokens in CSS for the element's own colours; --surface-* / --content-* / --border-* for non-interactive children.

Toggle pipeline example (Tabs):

export default function Tabs(props: TabsProps) {
  const { items = [], activeId, theme, semantic } = props;
  return (
    <div data-theme={theme} data-semantic={semantic ?? 'theme'}>
      {items.map(item => (
        <button
          key={item.id}
          {...interactiveAttrs({
            colorStyle: props.colorStyle ?? 'cta',
            substyle:   (props as any).substyle,
            toggle:     item.id === activeId ? 'selected' : 'unselected',
            theme,
            semantic,
          })}
        >
          {item.label}
        </button>
      ))}
    </div>
  );
}

Priority pipeline example (ProgressBar):

export default function ProgressBar(props: ProgressBarProps) {
  const { value = 0, max = 100, priority = 'primary', theme, semantic } = props;
  const pct = Math.min(100, Math.max(0, (value / max) * 100));
  return (
    <div data-theme={theme} data-semantic={semantic ?? 'theme'}>
      <div
        {...interactiveAttrs({ colorStyle: props.colorStyle ?? 'cta', priority, theme, semantic })}
        style={{ width: `${pct}%` }}
      />
    </div>
  );
}

Integration Guide

1. Install the CSS

// app entry point
import 'path/to/tokens-claude/tokens.css';

2. Set root theme

<html data-theme="dark">   <!-- default; use "light" for light mode -->

3. Alias the path

// vite.config.ts
resolve: { alias: { '@ds': '/path/to/tokens-claude' } }
import Button from '@ds/components/Button';
import { surface, token } from '@ds/tokens';

4. Migrate existing components

To wire an existing component into the token pipeline:

  1. Remove hardcoded hex colours — replace with var(--surface-*), var(--content-*), var(--border-*).
  2. For interactive elements, call interactiveAttrs and replace manual :hover / :focus CSS with var(--interactive-*).
  3. Remove hand-rolled state blocks in CSS — layers 05–06 handle them automatically via pseudo-classes.
  4. Ensure an ancestor carries data-theme and data-semantic.

Cascade isolation

Any element can create a new token context by setting a data-* attribute — the CSS cascade resolves everything downstream:

<!-- Dark page with a light-themed card inside it -->
<html data-theme="dark">
  <div class="card" data-theme="light"></div>
</html>

<!-- Grey scheme with a CTA button inside it -->
<section data-style="grey" data-grey="eclipse">
  <button data-style="cta" data-cta="nebula" data-priority="primary"></button>
</section>

<!-- Inverse tooltip over a light surface -->
<div class="tooltip" data-semantic="inverse"></div>

No JavaScript is needed for any of these — the CSS cascade handles all token resolution.


File Structure

css/
  index.css              Entry point — imports all layers in order
  01-primitives.css      Raw values
  02-theme.css           Light/dark palette
  03-sub-styles.css      Color family selectors
  04-style.css           Unified ---style-* scale
  05-priority-toggle.css State → style step mappings
  06-status.css          Pseudo-class activation rules + ring spreads
  07-semantic.css        Component-ready token names
  spacing.css
  typography.css
  size.css
  device-grid.css
  flags.css

components/
  interactiveAttrs.ts    Shared data-* attribute assembler for interactive components
  index.ts               Barrel re-export for all components and types
  Button.tsx             Reference implementation — priority pipeline
  Icon.tsx               Reference implementation — size tokens
  Card.tsx               Reference implementation — semantic surface
  *.tsx                  Typed stubs (see TODO in each file)

tokens.ts                TypeScript constants for all CSS variable names + dataAttr helpers

storybook/
  components/            Per-component story files
  pages/
  templates/

.storybook/
  main.ts                Story glob patterns and addon config
  preview.ts             Global decorators (theme, token CSS import)

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors