Shain Design Language System (DLS)
A token-based design system for product UI work. Components use semantic tokens only — no hardcoded colors, spacing, or typography. Figma is the source of truth; code is generated or guided by Figma exports.
For the full Figma best practices reference, read references/figma-best-practices.md.
For guidance on evolving the token system, read references/token-improvements.md.
1. Architecture
Three-tier token hierarchy
Primitive → Semantic → Component-specific (optional) colors/blue/500 → #0097DB action/primary/default → {colors/blue/500} button/bg/primary → {action/primary/default}
- Primitive — raw palette values (
colors/blue/500,spacing/4,radius/md). Never referenced directly from components. - Semantic — purpose-named aliases (
action/primary/default,text/primary,bg/surface). Components reference these. - Component-specific — optional per-component tokens when a component needs its
own tuning knob (
button/bg/primary). Use sparingly.
Sync flow
Figma Variables → figma-tokens.json → tokens/build.ts → tokens/dist/ ├── tokens.css └── tokens.ts
Project structure
├── docs/ Documentation ├── tokens/ figma-tokens.json → CSS vars + TS └── src/ ├── components/ React components └── icons/ Figma → lucide-react catalog
2. Design principles
- Token-first — All visual values come from design tokens. No hardcoded colors, spacing, or typography in components.
- Semantic over raw — Semantic token names describe purpose, not appearance.
Components reference
destructive/default, notred/500. - Figma as source of truth — Tokens and component specs live in Figma. Code is generated or guided by Figma exports.
- Mode-ready — All semantic tokens resolve through modes (Light/Dark, density, brand) so a single component definition adapts to any theme.
3. Token naming
Pattern
{category}/{purpose}/{state}
- category —
action,destructive,success,warning,info,text,bg,border,icon,overlay - purpose —
primary,secondary,subtle,inverse,link,disabled - state —
default,hover,pressed,focus,disabled,selected
Examples
| Figma variable | CSS custom property |
|---|---|
action/primary/default | --action-primary-default |
action/primary/hover | --action-primary-hover |
destructive/default | --destructive-default |
text/primary | --text-primary |
bg/surface | --bg-surface |
border/default | --border-default |
Rules
- Use lowercase, slashes in Figma, kebab-case as CSS vars.
- Do not encode appearance in the name (
red-hoveris wrong;destructive-hoveris right). - Stateful tokens always live at the third level (
.../hover,.../pressed). - A token that exists on one category should exist on comparable categories
(if
action/primary/hoverexists, so shoulddestructive/hover).
4. Modes
Built-in modes
- Theme: Light (default), Dark
- Optional: Density (Comfortable, Compact), Brand (if multi-brand)
Rules
- Every semantic token defines a value for every mode. No mode may be empty.
- Components never branch on theme — they reference semantic tokens only.
- Primitive collections do not need modes; modes live on the semantic layer.
Required dark-mode pairs (minimum)
| Semantic | Light | Dark |
|---|---|---|
bg/canvas | #FFFFFF | #0B0D12 |
bg/surface | #F7F8FA | #141821 |
text/primary | #0F1420 | #F2F4F8 |
text/secondary | #4A5261 | #A7B0BF |
border/default | #E4E7EC | #2A2F3A |
action/primary/default | #0097DB | #3AB7F2 |
5. Component guidelines
File layout
src/components/[ComponentName]/ ├── [ComponentName].tsx ├── [ComponentName].css └── index.ts
CSS class naming (BEM-like)
- Block:
shain-[component](e.g.shain-btn,shain-input) - Element:
shain-[component]__[element](e.g.shain-btn__label) - Modifier:
shain-[component]--[modifier](e.g.shain-btn--primary,shain-btn--sm)
Multiple modifiers combine: shain-btn shain-btn--primary shain-btn--md shain-btn--destructive.
Token usage in code
| Do | Don't |
|---|---|
background-color: var(--action-primary-default) | background-color: #0097DB |
padding: var(--spacing-2) var(--spacing-3) | padding: 8px 12px |
border-radius: var(--radius-md) | border-radius: 8px |
color: var(--text-primary) | color: #0F1420 |
Theme config pattern (variant styling)
For components with variants and states, use a config object instead of variant-specific CSS:
tsenum ButtonVariant { Primary = "primary", Secondary = "secondary", } const theme = { [ButtonVariant.Primary]: { base: ["--action-primary-default", "--action-primary-default", "--text-inverse"], hover: ["--action-primary-hover", "--action-primary-hover", "--text-inverse"], pressed: ["--action-primary-pressed", "--action-primary-pressed", "--text-inverse"], disabled: ["--action-disabled", "--border-default", "--text-disabled"], }, [ButtonVariant.Secondary]: { base: ["--bg-surface", "--border-default", "--action-primary-default"], hover: ["--bg-surface-hover", "--border-default", "--action-primary-hover"], pressed: ["--bg-surface-pressed", "--border-default", "--action-primary-pressed"], disabled: ["--bg-surface", "--border-default", "--text-disabled"], }, }; // Apply via CSS variables: --cmp-bg, --cmp-border, --cmp-color
Component anatomy
Interactive components support: variants (primary, secondary, outline, ghost, link), sizes (sm, md, lg, icon), states (default, hover, focus, pressed, disabled, loading).
Non-interactive components (e.g. Badge): no hover states or transitions, cursor: default.
Variant properties in Figma
Keep variant properties to 2–3 maximum. In exceptional cases 4 is acceptable with justification. Use boolean properties for optional elements (icons, badges) and instance swap for swappable elements.
Good — 9 variants + flexible toggles: Variant: Size (sm, md, lg) × Type (primary, secondary, outline) Boolean: Show Leading Icon, Show Trailing Icon Instance swap: Icon Bad — 864 variants: Size (4) × Color (6) × Icon Position (3) × State (4) × Alignment (3)
Props convention
- TypeScript interfaces with explicit types
- Support
classNameand spread remaining HTML attributes forwardReffor form elements and focusable components- Default values match the most common use case
Icons
- Use lucide-react
- Accept
LucideIcontype for icon props - Icon size: 16px inline with text; use tokens if a size scale exists
- Catalog:
src/icons/iconCatalog.tsmaps Figma names → Lucide components
6. Accessibility
- Focus —
:focus-visiblefor keyboard focus; avoid focus ring on mouse click. - Disabled —
disabledon interactive elements;aria-disabledonly as fallback. - Loading —
aria-busy="true"when appropriate. - Labels — All form controls have associated labels (visible or
aria-label). - Keyboard — Tab, Enter, Space, Escape for dropdowns/modals.
- Touch targets — Minimum 44×44px.
- Contrast — 4.5:1 for text, 3:1 for UI elements, verified in both Light and Dark.
7. Typography
- Font family: sans-serif (configurable per brand)
- Weights: 400 (normal), 600 (medium), 700 (bold)
- Sizes:
text/xs(12px) throughtext/3xl(32px) - Line heights:
leading/1(16px) throughleading/6(40px)
8. Spacing & Radius
Spacing scale:
| Token | Value |
|---|---|
spacing/0 | 0px |
spacing/0.5 | 2px |
spacing/1 | 4px |
spacing/2 | 8px |
spacing/3 | 12px |
spacing/4 | 16px |
spacing/5 | 20px |
spacing/6 | 24px |
Radius scale:
| Token | Value |
|---|---|
radius/none | 0px |
radius/xs | 2px |
radius/sm | 4px |
radius/md | 8px |
radius/lg | 12px |
radius/xl | 24px |
radius/full | 9999px |
9. Anti-patterns
- Absolute positioning instead of auto layout — Always use auto layout with spacing tokens.
- Too many variants — 100+ variants means booleans and instance swaps are underused.
- Hardcoded values — Any raw hex, px value, or magic number should be a token reference.
- Appearance-based names —
red-hover,blue-bgleak visuals into purpose names. - Components referencing primitives — Primitives are internal; components reference semantic tokens.
- Unnamed layers — "Frame 123" is unacceptable. Name every layer by purpose.
- No component docs — Every component needs a doc from the template.
- Mixing presentation and content — Separate structural layers from content layers.
- Componentizing everything — Only reusable, consistent, interactive, or coded elements.
- Ignoring accessibility in Figma — 44×44px targets, contrast ratios, focus states.
- Deep nesting — Maximum 3 levels (e.g., Card → Button → Icon).
- Detached instances — Always use main component references inside other components.
10. Checklists
Figma-side (before handoff)
Structure: auto layout, descriptive layer names, ≤3 nesting levels, main component refs, proper constraints.
Properties: 2–3 variant props, booleans for optional elements, text props for content, instance swap for icons, sensible defaults.
Tokens: all fills use semantic variables (no hex), spacing variables, text styles, effect styles, no magic numbers. No primitive references from component layers.
States: default, hover, pressed/active, disabled, focus (keyboard).
Testing: works at different sizes, short/long content, no overflow, Light and Dark mode both pass contrast checks.
Code-side (new components)
- All colors:
var(--semantic-token-name)— never primitives - Variant colors via theme config
- All spacing:
var(--spacing-N) - All typography:
var(--text-*)/var(--leading-*) - Border radius:
var(--radius-*) - CSS classes:
shain-[component]naming - TypeScript typed props
forwardReffor focusable elements:focus-visiblestyles- Component doc from template
11. AI workflow for component generation
When generating components from Figma:
- Read component spec — Variants, sizes, states from Figma.
- Map to semantic tokens — Every fill, stroke, text, padding, radius, gap maps to a semantic token (never a primitive).
- Use the theme config pattern for variants with multiple states.
- Follow component guidelines — BEM naming (
shain-*), forwardRef, accessibility. - Verify both modes — Component must pass contrast in Light and Dark.
- Add changelog entry to the component doc.
Reference files
Read these when you need deeper context:
| File | When to read |
|---|---|
references/figma-best-practices.md | Building/reviewing Figma components, design tokens architecture, platform-specific guidelines (iOS/Android/Web), export settings |
references/token-improvements.md | Planning token system changes, adding new tokens, restructuring collections, brand expansion |


