* chore: replace old css vars * fix: replace shadow and primary colors * chore: remove hardcoded colors
19 KiB
Design System Philosophy Guide
Overview
This guide explains the semantic design system philosophy for building consistent, maintainable UIs. The system is built on three core concepts: Canvas, Surface, and Layer.
Core Concepts
1. Canvas (bg-canvas)
What it is: The application-level background that serves as the foundation for all content. The canvas is the entire application background, not individual pages. There is only one canvas in the entire application, used at the root level.
When to use:
- Only at the application root - the single root container that wraps the entire application
- The main application background (not page backgrounds)
When NOT to use:
- ❌ Page-level backgrounds
- ❌ Nested containers
- ❌ Cards or components
- ❌ Modals or dropdowns
- ❌ Sidebars or panels
- ❌ Anywhere else in the application
Critical Rule: Canvas should only appear once in your entire application - at the root level. All pages, routes, and components sit on top of this single canvas.
Example:
// ✅ Correct: Canvas at application root (only place it should be)
// App.tsx or root layout
<div className="bg-canvas min-h-screen">
{/* All application content goes here */}
<Routes>
<Route path="/" element={<Page />} />
</Routes>
</div>;
// ✅ Correct: Pages use surfaces, not canvas
function Page() {
return <div className="bg-surface-1">{/* Page content */}</div>;
}
// ❌ Wrong: Canvas used for a page
function Page() {
return <div className="bg-canvas">{/* Don't use canvas here */}</div>;
}
// ❌ Wrong: Canvas used for a card
<div className="bg-canvas p-4 rounded-md">{/* Card content */}</div>;
2. Surface (bg-surface-1, bg-surface-2, bg-surface-3)
What it is: Top-level containers that sit directly on the canvas. Surfaces never overlap each other - they are siblings in the layout hierarchy.
When to use:
- Main content areas
- Sections of a page
- Primary containers
- Panels that sit side-by-side
Surface hierarchy:
bg-surface-1: Primary surface (most common)bg-surface-2: Secondary surface (for variation)bg-surface-3: Tertiary surface (rare, for special cases)
Rules:
- Surfaces are siblings, not nested (in the same plane)
- Each surface should use its corresponding layer for nested elements
- Surfaces provide the base for stacking layers
Exception - Different Planes:
- Modals, overlays, and popovers exist on a different plane (different z-index/stacking context)
- In these cases, it's acceptable to use a surface even when there's a surface below
- This is because they are visually and functionally separate from the underlying content
Example:
// ✅ Correct: Surfaces as siblings
<div className="bg-canvas">
<div className="bg-surface-1">
{/* Main content area */}
</div>
<div className="bg-surface-2">
{/* Secondary content area - sibling, not nested */}
</div>
</div>
// ✅ Correct: Page with header and main (same surface)
<div className="bg-surface-1">
<header className="border-b border-subtle">
{/* Header is part of the surface, not a separate surface */}
</header>
<main>
{/* Main is part of the surface, not a separate surface */}
</main>
</div>
// ❌ Wrong: Surface nested in surface (same plane)
<div className="bg-surface-1">
<div className="bg-surface-2">
{/* This breaks the philosophy */}
</div>
</div>
// ✅ Correct: Modal on different plane
<div className="bg-canvas">
{/* Main page content */}
<div className="bg-surface-1">
Page content
</div>
{/* Modal overlay - different plane */}
<div className="fixed inset-0 z-50">
<div className="bg-backdrop fixed inset-0" />
<div className="bg-surface-1 rounded-lg shadow-lg p-6">
{/* Modal can use surface-1 even though page uses surface-1 */}
Modal content
</div>
</div>
</div>
3. Layer (bg-layer-1, bg-layer-2, bg-layer-3)
What it is: Stacking layers that create depth within a surface. Layers stack on top of each other in a specific order.
When to use:
- Cards within a surface
- Group headers
- Nested containers
- Dropdowns and modals
- Sidebars
- Any element that needs to appear "on top" of a surface
Layer hierarchy:
bg-layer-1: First layer (closest to surface)bg-layer-2: Second layer (on top of layer-1)bg-layer-3: Third layer (on top of layer-2)
Critical Rule - Layer-to-Surface Association:
bg-surface-1→ usebg-layer-1for nested elementsbg-surface-2→ usebg-layer-2for nested elementsbg-surface-3→ usebg-layer-3for nested elements
Rare Exception - Visual Separation:
In very rare cases, you may go one level above for visual separation when needed for specific UI elements:
- Inputs in modals (modal has
bg-surface-1, input can usebg-layer-2for separation) - Buttons, switches, and form controls that need more visual distinction
- Important: This is very rare and should only be used for interactive form elements, not for content boxes or cards
Example:
// ✅ Correct: Surface-1 with layer-1
<div className="bg-surface-1 p-4">
<div className="bg-layer-1 hover:bg-layer-1-hover rounded-md p-3">
Card content
</div>
</div>
// ✅ Correct: Surface-2 with layer-2
<div className="bg-surface-2 p-4">
<div className="bg-layer-2 hover:bg-layer-2-hover rounded-md p-3">
Card content
</div>
</div>
// ❌ Wrong: Surface-1 with layer-2 (for content boxes)
<div className="bg-surface-1 p-4">
<div className="bg-layer-2">
{/* Wrong layer for this surface - use layer-1 for content boxes */}
</div>
</div>
// ✅ Correct: Rare exception - Input in modal for visual separation
<div className="bg-surface-1 rounded-lg p-6">
<form>
<input className="bg-layer-2 border border-subtle rounded-md px-3 py-2" />
{/* Input uses layer-2 for visual separation from modal surface */}
</form>
</div>
Stacking Layers
How to Stack Layers
Layers stack in order: surface → layer-1 → layer-2 → layer-3
Pattern:
Canvas
└── Surface
└── Layer 1 (first level of depth)
└── Layer 2 (second level of depth)
└── Layer 3 (third level of depth)
Example - Proper Stacking:
// ✅ Correct: Proper layer stacking
<div className="bg-surface-1 p-4">
{/* Layer 1: Card */}
<div className="bg-layer-1 hover:bg-layer-1-hover rounded-md p-4">
<h3>Card Title</h3>
{/* Layer 2: Nested section */}
<div className="bg-layer-2 hover:bg-layer-2-hover rounded p-2 mt-2">
Nested content
{/* Layer 3: Deeply nested */}
<div className="bg-layer-3 hover:bg-layer-3-hover rounded p-1 mt-1">Deep content</div>
</div>
</div>
</div>
When to use each layer:
- Layer 1: Most common - cards, headers, primary nested elements
- Layer 2: Secondary depth - nested cards, sub-sections
- Layer 3: Deep nesting - rarely needed, for complex hierarchies
State Variants
Hover States
Critical Rule: Hover must always match the base background layer.
Pattern: bg-layer-X hover:bg-layer-X-hover
Examples:
// ✅ Correct: Matching hover
<div className="bg-layer-1 hover:bg-layer-1-hover">
Hoverable element
</div>
<div className="bg-layer-2 hover:bg-layer-2-hover">
Hoverable element
</div>
// ❌ Wrong: Mismatched hover
<div className="bg-layer-1 hover:bg-layer-2-hover">
{/* Never do this */}
</div>
Active States
Use -active variants when an element is in an active/pressed state.
// ✅ Correct: Active state
<button
className={cn("bg-layer-1 hover:bg-layer-1-hover", {
"bg-layer-1-active": isActive,
})}
>
Button
</button>
Selected States
Use -selected variants only when there's actual selection logic.
// ✅ Correct: Selected state with logic
<div className={cn(
"bg-layer-1 hover:bg-layer-1-hover",
{
"bg-layer-1-selected": isSelected
}
)}>
Selectable item
</div>
// ✅ Correct: With data attribute
<div className="bg-layer-1 hover:bg-layer-1-hover data-[selected]:bg-layer-1-selected">
Selectable item
</div>
Common Patterns
Pattern 1: Application Root Layout
// ✅ Correct: Application root (only place for canvas)
// App.tsx or root layout
<div className="bg-canvas min-h-screen">
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/dashboard" element={<DashboardPage />} />
</Routes>
</div>;
// ✅ Correct: Individual page structure
function HomePage() {
return (
<div className="bg-surface-1">
{/* Header - part of the page surface, uses layer for depth */}
<header className="border-b border-subtle">
<div className="bg-layer-1 hover:bg-layer-1-hover px-4 py-2">Header content</div>
</header>
{/* Main content - part of the page surface */}
<main className="p-6">
<div className="bg-layer-1 hover:bg-layer-1-hover rounded-md p-4">Content card</div>
</main>
</div>
);
}
Pattern 2: Card with Nested Elements
// ✅ Correct: Card with proper layering
<div className="bg-surface-1 p-4">
<div className="bg-layer-1 hover:bg-layer-1-hover rounded-md p-4">
<h3 className="text-primary font-semibold">Card Title</h3>
{/* Nested section */}
<div className="bg-layer-2 hover:bg-layer-2-hover rounded p-3 mt-3">
<p className="text-secondary">Nested content</p>
</div>
</div>
</div>
Pattern 3: Dropdown/Modal
// ✅ Correct: Modal structure (different plane exception)
function PageWithModal() {
return (
<div className="bg-surface-1">
{/* Main page content */}
<div>Page content</div>
{/* Modal - different plane, can use surface even with surface below */}
{isModalOpen && (
<div className="fixed inset-0 z-50">
<div className="bg-backdrop fixed inset-0" />
<div className="bg-surface-1 rounded-lg shadow-lg p-6">
<div className="bg-layer-1 hover:bg-layer-1-hover rounded p-2">Modal content</div>
</div>
</div>
)}
</div>
);
}
Pattern 4: Sidebar Layout
// ✅ Correct: Sidebar with main content (page level, not app root)
// Both sidebar and main are siblings on the same surface
// Sidebar menu items use transparent backgrounds with hover states
function DashboardPage() {
return (
<div className="bg-surface-1 flex">
{/* Sidebar - part of the page surface */}
<aside className="border-r border-subtle w-64">
<div className="hover:bg-layer-1-hover p-4">Sidebar item</div>
</aside>
{/* Main content - part of the page surface */}
<main className="flex-1 p-6">
<div className="bg-layer-1 hover:bg-layer-1-hover rounded-md p-4">Main content</div>
</main>
</div>
);
}
Pattern 5: List with Items
// ✅ Correct: List structure
<div className="bg-surface-1 p-4">
<div className="bg-layer-1 hover:bg-layer-1-hover rounded-md mb-2 p-3">List item 1</div>
<div className="bg-layer-1 hover:bg-layer-1-hover rounded-md mb-2 p-3">List item 2</div>
<div className="bg-layer-1 hover:bg-layer-1-hover rounded-md p-3">List item 3</div>
</div>
Pattern 6: Form with Inputs
// ✅ Correct: Form structure
<div className="bg-surface-1 p-6">
<form className="bg-layer-1 rounded-md p-4 space-y-4">
<div>
<label className="text-primary font-medium">Name</label>
{/* Input can use bg-surface-1 or bg-layer-2 for visual separation (rare exception) */}
<input className="bg-surface-1 border border-subtle rounded-md px-3 py-2 text-primary" type="text" />
</div>
<button className="bg-layer-2 hover:bg-layer-2-hover rounded-md px-4 py-2 text-primary">Submit</button>
</form>
</div>
// ✅ Correct: Input in modal (rare exception for visual separation)
<div className="bg-surface-1 rounded-lg p-6">
<form>
<label className="text-primary font-medium">Name</label>
{/* Input uses layer-2 for visual separation from modal surface - rare exception */}
<input className="bg-layer-2 border border-subtle rounded-md px-3 py-2 text-primary" type="text" />
</form>
</div>
Decision Tree
When to use Canvas?
Is this the root container of the entire application?
(Only one place in the whole app)
├─ YES → Use bg-canvas (application root only)
└─ NO → Continue to Surface decision
When to use Surface?
Is this a top-level container that sits on canvas?
AND
Is it a sibling to other containers (not nested)?
OR
Is this a modal/overlay on a different plane (z-index)?
├─ YES → Use bg-surface-1 (or surface-2/3 for variation)
└─ NO → Continue to Layer decision
When to use Layer?
Is this nested within a surface?
├─ YES → Use bg-layer-1 (or layer-2/3 for deeper nesting)
│ Match layer number to surface number
└─ NO → Re-evaluate: Should this be a surface?
Text Colors
Use semantic text colors that match the hierarchy:
text-primary: Main text, headings, important contenttext-secondary: Secondary text, descriptionstext-tertiary: Tertiary text, labels, metadatatext-placeholder: Placeholder text, hints
Example:
<div className="bg-layer-1 p-4">
<h2 className="text-primary font-semibold">Title</h2>
<p className="text-secondary">Description text</p>
<span className="text-tertiary text-13">Metadata</span>
<input className="placeholder-(--text-color-placeholder)" placeholder="Enter text..." />
</div>
Border Colors
Use semantic border colors:
border-subtle: Subtle borders, dividersborder-subtle-1: Slightly more visible bordersborder-strong: Strong borders, emphasisborder-strong-1: Very strong borders
Example:
<div className="bg-layer-1 border border-subtle rounded-md p-4">
<div className="border-b border-subtle-1 pb-2 mb-2">Section with divider</div>
</div>
Common Mistakes to Avoid
❌ Mistake 1: Canvas for Pages or Cards
// ❌ Wrong: Canvas used for a page
function Page() {
return <div className="bg-canvas">Page content</div>;
}
// ❌ Wrong: Canvas used for a card
<div className="bg-canvas rounded-md p-4">Card content</div>;
// ✅ Correct: Pages use surfaces
function Page() {
return (
<div className="bg-surface-1">
<div className="bg-layer-1 rounded-md p-4">Card content</div>
</div>
);
}
❌ Mistake 2: Nested Surfaces (Same Plane)
// ❌ Wrong: Nested surfaces in same plane
<div className="bg-surface-1">
<div className="bg-surface-2">
Nested surface
</div>
</div>
// ✅ Correct: Use layer instead
<div className="bg-surface-1">
<div className="bg-layer-1">
Nested layer
</div>
</div>
// ✅ Correct: Exception - Modal on different plane
<div className="bg-canvas">
<div className="bg-surface-1">Page content</div>
<div className="fixed inset-0 z-50">
<div className="bg-surface-1">Modal (different plane)</div>
</div>
</div>
❌ Mistake 3: Wrong Layer for Surface
// ❌ Wrong: surface-1 with layer-2 (for content boxes)
<div className="bg-surface-1">
<div className="bg-layer-2">
Content box
</div>
</div>
// ✅ Correct: surface-1 with layer-1 (for content boxes)
<div className="bg-surface-1">
<div className="bg-layer-1">
Content box
</div>
</div>
// ✅ Correct: Rare exception - Input/button for visual separation
<div className="bg-surface-1">
<input className="bg-layer-2" />
{/* Input uses layer-2 for visual separation - very rare exception */}
</div>
Note: Going one level above (e.g., bg-layer-2 with bg-surface-1) is only valid in very rare cases for interactive form elements (inputs, buttons, switches) that need extra visual separation. For content boxes, cards, and normal nested elements, always match the layer number to the surface number.
❌ Mistake 4: Mismatched Hover
// ❌ Wrong
<div className="bg-layer-1 hover:bg-layer-2-hover">
Content
</div>
// ✅ Correct
<div className="bg-layer-1 hover:bg-layer-1-hover">
Content
</div>
❌ Mistake 5: Missing Hover Prefix
// ❌ Wrong
<div className="bg-layer-1-hover">
Content
</div>
// ✅ Correct
<div className="bg-layer-1 hover:bg-layer-1-hover">
Content
</div>
Best Practices
-
Canvas is Application Root Only: Canvas should only appear once in your entire application - at the root level (App.tsx or root layout). All pages use surfaces, not canvas.
-
Use Surfaces for Pages and Top-Level Containers: Surfaces are siblings, not nested (except modals/overlays on different planes)
-
Match Layers to Surfaces: surface-1 → layer-1, surface-2 → layer-2, etc. (Very rare exception: form elements can go one level above for visual separation)
-
Stack Layers Properly: Use layer-1 first, then layer-2, then layer-3 as needed
-
Always Match Hover States: If base is
bg-layer-X, hover must behover:bg-layer-X-hover -
Use Semantic Text Colors: Match text color to importance (primary, secondary, tertiary, placeholder)
-
Keep It Simple: Don't over-nest. Most components only need layer-1
-
Test Visual Hierarchy: Ensure the stacking creates clear visual depth
Quick Reference
Hierarchy Structure
Canvas (one per page)
└── Surface 1 (top-level container)
└── Layer 1 (first level of depth)
└── Layer 2 (second level)
└── Layer 3 (third level)
Common Combinations
- Application Root:
bg-canvas(only one place in entire app) - Single Surface Page:
bg-surface-1→bg-layer-1 - Multiple Surfaces Page: Grid of
bg-surface-1(orbg-surface-2) as siblings - Card:
bg-surface-1→bg-layer-1 hover:bg-layer-1-hover - Nested Card:
bg-surface-1→bg-layer-1→bg-layer-2 - Sidebar Layout:
bg-surface-1(sidebar) +bg-surface-1(main) - both siblings - Sidebar Menu Items: Transparent background with
hover:bg-layer-1-hover(no basebg-layer-1)
State Variants
- Hover:
bg-layer-X hover:bg-layer-X-hover - Active:
bg-layer-X-active(when pressed/active) - Selected:
bg-layer-X-selected(when selected)
Alignment Checklist
When reviewing components, ensure:
- Canvas is only used at the application root (one place in entire app)
- Pages use surfaces, not canvas
- Surfaces are siblings, not nested (except modals/overlays on different planes)
- Layers match their surface (surface-1 → layer-1) - except rare cases for form elements
- Sidebar menu items use transparent backgrounds with hover states
- Hover states match base layers
- Layers stack properly (1 → 2 → 3)
- Text colors are semantic (primary, secondary, tertiary, placeholder)
- Borders use semantic colors (subtle, strong)
- No unnecessary nesting
- Visual hierarchy is clear
- State variants are used correctly
- Modals/overlays use surfaces (exception to nesting rule)