Skip to main content

Design System

Elftia's visual design pursues warmth, affinity, and sophistication while avoiding cold, purely technical aesthetics.


Design Philosophy

Color Principles

  • Warm neutral colors: All surface colors have a slight warm tone (hue 24-38), not pure grayscale
  • Dark mode: Warm charcoal instead of pure black
  • Light mode: Warm cream instead of pure white
  • No cold grays: Don't use pure gray (hsl(0, 0%, ...)) for backgrounds or borders

Corner Radius Principles

ElementRadiusTailwind Class
Cards/containers12pxrounded-xl
Input fields12pxrounded-xl
Buttons/badges6pxrounded-md
Default8pxrounded-lg

Avoid corner radius smaller than 4px (except for line decorations).

Typography Principles

PurposeFontTailwind Class
Display/large headlinesNoto Serif / Georgiafont-display
Body/interfaceInterfont-sans
CodeJetBrains Monofont-mono

Use font-semibold (not font-bold) for headlines, paired with tracking-tight.


Color System

Semantic Tokens

All colors defined via CSS variables mapped to utility classes in Tailwind configuration. Never use hard-coded color values.

// Don't do this
<div className="bg-white text-black border-gray-200">
<div style={{ backgroundColor: '#ffffff' }}>

// Correct
<div className="bg-surface-0 text-foreground border-border">
<div style={{ backgroundColor: 'var(--surface-0)' }}>

Common Token Reference

PurposeTailwind ClassCSS Variable
Page backgroundbg-backgroundvar(--background)
L0 backgroundbg-surface-0var(--surface-0)
L1 background (cards/sidebar)bg-surface-1var(--surface-1)
L2 background (inputs/secondary containers)bg-surface-2var(--surface-2)
L3 background (popovers)bg-surface-3var(--surface-3)
Main texttext-foregroundvar(--foreground)
Secondary texttext-muted-foregroundvar(--muted-foreground)
Auxiliary texttext-text-subtlevar(--text-subtle)
Borderborder-bordervar(--border)
Theme colorbg-primary / text-primaryvar(--primary)
Successtext-successvar(--success)
Errortext-destructivevar(--destructive)
Warningtext-warningvar(--warning)

Dark/Light Mode

Switching Mechanism

Use Tailwind's class strategy (darkMode: 'class'), centrally managed via ThemeContext.

// Correct: get theme info via useTheme
import { useTheme } from '@/shared/state/themeStore';

function MyComponent() {
const { mode, resolvedMode, userTheme } = useTheme();
}

// Don't: manual detection
const isDark = localStorage.getItem('theme') === 'dark';
const isDark = window.matchMedia('(prefers-color-scheme: dark)').matches;

Hierarchy System

Dark mode relies on brightness to distinguish hierarchy: closer UI elements to user have brighter backgrounds.

LevelTokenBrightnessPurposeReference Color
L0--surface-07%Page main background#121212
L1--surface-112%Sidebar, cards#1E1E1E
L2--surface-217%Secondary containers, input#2B2928
L3--surface-322%Popover, Dropdown#383635

Brightness difference between levels must be >= 4-5% to ensure visual distinction.

Border Standards

In dark mode, human perception of dark areas is less sensitive, so borders need higher visibility:

ScenarioLight ModeDark Mode
Card borderborder-border/30 ~ /40border-border/50 ~ /70
Dividerborder-border/20 ~ /30border-border/40 ~ /50
Input fieldborder-border/40border-border/60 ~ border-border

WCAG Accessibility

Based on WCAG 2.1 standards.

Contrast Requirements

Element TypeMin ContrastDescription
Normal text (< 18pt)4.5:1Body, descriptions, labels
Large text (>= 18pt or 14pt bold)3:1Headlines
UI controls (icons, borders)3:1Input borders, icons, badges
Disabled stateExempt, suggest 2.5:1Avoid complete invisibility

Text Token Usage Rules

TokenBrightnessAllowed BackgroundsTypical Use
text-foreground (93%)HighestAll surfacesMain headlines, body text
text-muted-foreground (65%)Mediumsurface-0, surface-1Secondary text, descriptions
text-text-subtle (50%)Lowsurface-0 onlyTimestamps, metadata
// Good: description text uses text-muted
<p className="text-muted-foreground">5 models total</p>

// Bad: using text-subtle on surface-1 card (insufficient contrast)
<div className="bg-surface-1">
<span className="text-text-subtle">Hard to read</span>
</div>

Color Communicates Information

Never rely on color alone to convey status, must pair with text label or icon:

// Bad: color only
<div className={status === 'error' ? 'border-red-500' : 'border-border'} />

// Good: color + icon + text
<div className={status === 'error' ? 'border-destructive' : 'border-border'}>
{status === 'error' && <AlertCircle className="text-destructive" />}
<span>{errorMessage}</span>
</div>

Focus State

Use focus-visible to provide focus border for keyboard navigation users:

// Good
<button className="focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none">
Button
</button>

// Don't: remove focus styles
<button className="outline-none focus:outline-none">Button</button>

Motion Preference

Respect system prefers-reduced-motion setting:

<div className="motion-safe:animate-fadeIn motion-reduce:animate-none">
Content
</div>

UI Component Standards

Don't Use Native Controls

Native ControlProject ComponentPath
<select>Select@/components/ui/select
<input type="text">Input@/components/ui/input
<input type="checkbox">Switch / Checkbox@/components/ui/switch
<button>Button@/components/ui/button
window.confirm()ConfirmDialog@/components/ui/confirm-dialog
window.alert()Toast component-

All dropdown components should support:

  • Viewport-aware positioning (use useDropdownPosition Hook)
  • Close on outside click
  • Close on Escape key
  • Item theme colors and styles

Wallpaper Transparency System

When user sets wallpaper, add data-wallpaper-active="true" attribute to body, triggering CSS transparency rules.

CSS Rule Hierarchy

PrioritySelectorEffectPurpose
1.bg-background, .bg-surface-0Fully transparentPage main background
2.wallpaper-blur35% opaque + blurMain container (WorkspaceShell)
3.wallpaper-blur .wallpaper-blurTransparent + blur*0.67Nested container (avoid stacking)
4bg-surface-0/XX in .wallpaper-blurTransparentLayout panels
5bg-surface-1/XX in .wallpaper-blur12% + blurContent cards (alpha variant)
6bg-surface-1, bg-popover15% + blurButtons/cards (exclude input)
7bg-surface-220% + blurSecondary container (exclude input)
8.wallpaper-panel85% + blur*1.33Floating dropdown/menu
9.wallpaper-solidOpaque surface-0Dialog/Modal

Hierarchy Stacking Model

WorkspaceShell (wallpaper-blur, 35%)
+-- Sidebar (bg-surface-0/75 -> transparent) = 35%
+-- Main content (bg-background -> transparent) = 35%
| +-- Content cards (bg-surface-1/80 -> 12%) ~ 43%
| +-- Buttons (bg-surface-1 -> 15%) ~ 45%
| +-- Input (input -> solid) = Opaque
| +-- Dropdown panel (wallpaper-panel -> 85%) = 85%
+-- Bottombar (wallpaper-blur -> transparent) = 35%

Wallpaper CSS Class Usage Guide

ScenarioRecommended
Page main containerbg-background or bg-surface-0 (auto fully transparent)
Button/cardbg-surface-1 or bg-surface-1/80 (auto semi-transparent)
Secondary containerbg-surface-2 (auto 20% semi-transparent)
Main layout containerAdd wallpaper-blur class
Floating dropdown/menuAdd wallpaper-panel class
Dialog/ModalAdd wallpaper-solid class
Input field<input> / <textarea> + bg-surface-1 (auto exclude, keep solid)

Components with Built-in Wallpaper Support

Semi-transparent frosted glass (wallpaper-panel):

  • Select dropdown panel
  • DropdownMenuContent / DropdownMenuSubContent
  • ContextMenuContent / ContextMenuSubContent

Fully opaque (wallpaper-solid):

  • DialogContent

Don't

  • Don't use bare bg-surface-0 as card background (disappears fully transparent in wallpaper mode)
  • Don't use bg-white, bg-black, bg-gray-*, bg-neutral-* hard-coded colors
  • For opaque effect, add wallpaper-solid class simultaneously

User-Customizable Color Overlay Layers (0.1.11+)

On top of the "basic wallpaper transparency system" above, the wallpaper panel exposes three categories of user-adjustable color layers, each driven by body data attributes + CSS variables, CSS selectors layer-wise overriding default surface behavior. All writes managed centrally by themeUtils.applyWallpaperToDocument, components must not directly body.style.setProperty.

1. Dimming Layer (body::before pseudo-element)

Data AttributeTriggerCSS Variable
data-wallpaper-active="true"Any wallpaper source ready--wp-dimming (0–1), --wp-dim-{h,s,l}
data-wp-dim-gradient="true"wallpaperDimmingGradient set--wp-dim-gradient (override HSL)

Brightness fallback: when --wp-dim-l not set, light mode defaults to 100%, dark mode to 0% (corresponding to original white/black behavior).

2. Element Surface Layer (sidebar / cards / tabs / context menu)

Data AttributeTriggerCSS Variable
data-wp-element-tint="true"wallpaperElementTint is valid hex--wp-elem-{h,s,l}
data-wp-element-gradient="true"wallpaperElementGradient set--wp-elem-gradient-{15,20,35} (by surface tier alpha versions)

Design key: each surface tier keeps independent alpha (surface-1 = 15%, surface-2 = 20%, .wallpaper-card = 35%), so even if changed to same tone, visual hierarchy remains distinguishable. Input/Textarea and wallpaper-solid / wallpaper-panel selectors are :not(...) excluded — readability over color consistency.

3. Message Bubble Layer (user / assistant separate)

Data AttributeTriggerCSS Variable
data-wp-bubble-override="true"wallpaperBubbleOverride === true--wp-bubble-alpha (0–1)
data-wp-bubble-tint-user="true"User bubble hex valid--wp-bubble-user-{h,s,l}
data-wp-bubble-tint-assistant="true"Assistant bubble hex valid--wp-bubble-asst-{h,s,l}
data-wp-bubble-gradient-{user,assistant}="true"Corresponding gradient set--wp-bubble-{user,asst}-gradient

CSS selectors cascade in two layers:

/* Layer 1: when override off, bubbles follow element tint (inherit) */
body[data-wp-element-tint="true"]:not([data-wp-bubble-override="true"]) .chat-bubble-user,
body[data-wp-element-tint="true"]:not([data-wp-bubble-override="true"]) .chat-bubble-assistant {
background: hsl(var(--wp-elem-h) var(--wp-elem-s) var(--wp-elem-l) / var(--wp-alpha-35, 0.35)) !important;
}

/* Layer 2: when override on, per-side tint + custom alpha applies */
body[data-wp-bubble-override="true"][data-wp-bubble-tint-user="true"] .chat-bubble-user {
background: hsl(var(--wp-bubble-user-h) var(--wp-bubble-user-s) var(--wp-bubble-user-l) / var(--wp-bubble-alpha, var(--wp-alpha-35, 0.35))) !important;
}

Layer 2 selector has higher specificity + appears later, so with !important it wins on tie.

When Adding New user-tint Field

  1. Add field in both ThemePreferences (settings-types.ts) and ThemePreferencesSchema (configSchema.ts)
  2. Add setter branch in ThemeService.setWallpaperPreferences + default values in readPreferences/importProfile/resetTheme/mergePreferences
  3. Add field in both Zod schemas in ThemeRouter (themeProfileSchema and inline schema in theme:setWallpaperPreferences)
  4. Add context value in ThemeContext + setWallpaperPreferences param + commitState fallback
  5. Add param in themeUtils.applyWallpaperToDocument + _prev* cache + body attribute/CSS variable writes
  6. Add UI in WallpaperPanel (toggle/color picker/slider) + i18n three languages
  7. Add selector in index.css (note hierarchy order and !important priority)
  8. Passthrough chain: AppearanceTabSettings.tsx / ThemeStudioPage.tsx (including DraftState + effective + Apply submit)
  9. Agent stubs: desktop-api.ts (preload contract), shared/agent/types/settings.ts (shared interface), shared/agent/web/theme.ts (HTTP implementation)
  10. Update this table + architecture-index SKILL.md field quick reference + ipc-channels.md theme:setWallpaperPreferences field table + appearance.md user documentation

Theme Compatible Development Rules

Use Only Semantic Tokens

Never write #fff/rgb() or Tailwind default colors directly.

Uniformly Use ThemeContext

When component needs theme info, read via useTheme().

Respect Configurable Fonts

For text/code areas use CSS variables:

<div style={{ fontFamily: 'var(--font-ui)' }}>Normal text</div>
<code style={{ fontFamily: 'var(--font-code)' }}>Code</code>

// Or use Tailwind classes
<div className="font-ui">Normal text</div>
<code className="font-code">Code</code>

Allow customCss Override

Avoid !important and large inline styles, prioritize className + CSS variables.


Self-Test Checklist

When creating new UI components:

  • Use only semantic tokens (no hard-coded colors)
  • Get theme info via useTheme()
  • Test dark/light mode switching
  • Test wallpaper transparency effect
  • Normal text contrast >= 4.5:1
  • Dark mode borders use dark:border-border/50 or higher
  • text-subtle used only on surface-0 background
  • Interactive elements use semantic tags or add role + tabIndex
  • Focus style uses focus-visible:ring-2
  • Text and borders discernible at 30% screen brightness
  • Layout doesn't break when window scaled to 200%