Skip to content

Theme System

DoCurious has a comprehensive theme system that converts a ThemeConfig object into CSS custom properties applied to :root. It supports 7 built-in presets, two navigation modes (sidebar vs topnav), three density levels, and persistence via localStorage.

Architecture

ThemeConfig Interface

The complete theme configuration is defined in src/theme/types.ts:

typescript
interface ThemeConfig {
  colors: ThemeColors
  typography: ThemeTypography
  spacing: ThemeSpacing
  branding: ThemeBranding
  style: ThemeStyle
  layout: ThemeLayout
}

Colors

typescript
interface ThemeColors {
  // Core
  primary: string           // Main brand color
  primaryHover: string
  primaryActive: string
  accent: string            // Highlight/accent color
  accentHover: string

  // Backgrounds
  background: string        // Page background
  backgroundSecondary: string

  // Text
  text: string              // Primary text
  textMuted: string         // Secondary text
  border: string

  // Status
  error: string
  success: string
  warning: string

  // Sidebar
  sidebarBackground: string
  sidebarText: string
  sidebarTextMuted: string
  sidebarActiveBackground: string
  sidebarActiveBorder: string

  // Audience accents (optional)
  accentB2C?: string        // Parents & Kids
  accentSchools?: string    // Schools & Educators
  accentProviders?: string  // Activity Providers

  // Secondary brand (optional)
  secondary?: string
  secondaryHover?: string
}

Typography

typescript
interface ThemeTypography {
  fontFamily: string          // Body text font
  fontFamilyHeading: string   // Heading font
  fontFamilyMono: string      // Code/monospace font
}

Style

Controls visual treatment of surfaces, cards, buttons, and spacing:

typescript
interface ThemeStyle {
  // Card surface
  cardShadow: string
  cardBorderWidth: string
  cardBorderRadius: string

  // Headings
  headingWeight: string
  headingLetterSpacing: string
  headingTextTransform: 'none' | 'uppercase' | 'capitalize'

  // Buttons
  buttonRadius: string
  buttonWeight: string

  // Sidebar / nav
  sidebarWidth: string
  navItemRadius: string
  navItemBorderWidth: string

  // Shadows
  surfaceShadowSm: string
  surfaceShadowMd: string
  surfaceShadowLg: string

  // Borders
  borderWidth: string
  dividerStyle: string

  // Motion
  transitionSpeed: string

  // Background texture
  backgroundTexture: string

  // Badge / input
  badgeRadius: string
  inputRadius: string
  inputBorderWidth: string

  // Density
  density: 'compact' | 'comfortable' | 'spacious'

  // Layout structure
  contentMaxWidth: string
  cardGap: string

  // Structural effects
  sidebarInset: string
  sidebarRadius: string
  cardAccentBorder: string
  cardBackground: string
  cardBackdropBlur: string
  headingBorderBottom: string
}

Layout

typescript
type NavigationMode = 'sidebar' | 'topnav'
type FilterPlacement = 'sidebar' | 'topbar' | 'drawer'

interface ThemeLayout {
  navigationMode: NavigationMode
  exploreFilterPlacement: FilterPlacement
  topNavHeight: string
}

Default Values

The default theme (src/theme/defaults.ts) uses the DoCurious brand palette:

TokenColorName
Primary#3D5A3CScout Green
Primary Hover#4A6B49
Accent#D4A84BHoney
Background#FAF8F3Cream
Background Secondary#F3EFE6
Text#1A1A1AInk
Text Muted#6B6B6BStone
Warning#E07B5ACoral
Secondary#5B8FA8Sky

Typography defaults:

  • Body: "Inter", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif
  • Headings: "Lexend", -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif
  • Mono: SFMono-Regular, Menlo, Monaco, Consolas, monospace

Theme Presets

Seven presets are defined in src/theme/presets/:

IDNameDescriptionDark?Swatches
trailheadTrailheadHome base -- warm Scout Green, Honey accents, Cream canvasNo#3D5A3C #D4A84B #FAF8F3
emberEmberDark and cozy -- Coral firelight glow, warm shadows, spaciousYes#E07B5A #D4A84B #1C1614
tidepoolTidepoolCoastal and clean -- Sky blue, pill buttons, no shadows, compactNo#5B8FA8 #D4A84B #F6F9FB
canopyCanopyDeep forest -- rich greens, Violet wildflower accents, immersiveNo#2D4A2C #8B7BC8 #F4F7F2
expeditionExpeditionBold adventure -- rugged borders, topo-map texture, uppercase headingsNo#8B5E2A #E07B5A #FBF7F0
stardustStardustMystical night -- Violet glow, Honey stars, dreamy transitionsYes#8B7BC8 #D4A84B #14121C
fieldJournalField JournalWarm and tactile -- dashed borders, paper texture, naturalist notebook feelNo#5b7c3a #b8860b #3a3226

Each preset is a complete ThemeConfig object. Presets are accessed via:

typescript
import { themePresets, getPresetById } from '../theme/presets'

const ember = getPresetById('ember')
// ember.config → ThemeConfig
// ember.isDark → true
// ember.swatches → ['#E07B5A', '#D4A84B', '#1C1614']

Layout Modes

The default mode. A fixed-width sidebar on the left with the main content area to the right. The sidebar width is controlled by style.sidebarWidth (default: 200px).

Top Navigation (navigationMode: 'topnav')

A horizontal navigation bar at the top. The height is controlled by layout.topNavHeight (default: 64px). The data-nav-mode attribute on <html> is set to 'topnav', which CSS can target for layout adjustments.

Filter Placement

The explore page filter panel placement is controlled by layout.exploreFilterPlacement:

  • 'sidebar' -- filters in a left sidebar
  • 'topbar' -- filters in a horizontal bar above content
  • 'drawer' -- filters in a slide-out drawer

ThemeProvider

The ThemeProvider component (src/theme/ThemeProvider.tsx) wraps the entire app:

  1. Loads the saved theme from themeApi.getTheme() on mount
  2. Falls back to defaultThemeConfig on error
  3. Calls applyThemeVars(config) on every config change to update CSS variables
  4. Provides the config and a change callback via ThemeConfigContext
tsx
// src/App.tsx (simplified)
import { ThemeProvider } from './theme'

function App() {
  return (
    <ThemeProvider>
      <RouterProvider router={router} />
    </ThemeProvider>
  )
}

CSS Variable Application

The applyThemeVars function (src/theme/applyTheme.ts) sets 60+ CSS custom properties on document.documentElement.style:

typescript
// Colors
--primary, --primary-hover, --primary-active, --primary-foreground
--accent, --accent-hover
--background, --background-secondary
--foreground, --foreground-muted
--border, --error, --success, --warning
--secondary, --secondary-hover
--sidebar-bg, --sidebar-text, --sidebar-text-muted
--sidebar-active-bg, --sidebar-active-border
--accent-b2c, --accent-schools, --accent-providers

// Typography
--font-body, --font-heading, --font-mono

// Card
--theme-card-shadow, --theme-card-border-width, --theme-card-radius

// Headings
--theme-heading-weight, --theme-heading-letter-spacing, --theme-heading-text-transform

// Buttons
--theme-button-radius, --theme-button-weight

// Sidebar/nav
--theme-sidebar-width, --theme-nav-item-radius, --theme-nav-item-border-width

// Shadows
--theme-shadow-sm, --theme-shadow-md, --theme-shadow-lg

// Borders
--theme-border-width, --theme-divider-style

// Motion
--theme-transition

// Density spacing (varies by compact/comfortable/spacious)
--theme-space-xs through --theme-space-2xl
--theme-section-gap, --theme-card-padding, --theme-page-padding

// Layout structure
--theme-content-max-width, --theme-card-gap
--theme-sidebar-inset, --theme-sidebar-radius
--theme-card-accent-border, --theme-card-bg, --theme-card-backdrop-blur
--theme-heading-border-bottom
--theme-topnav-height

The data-nav-mode attribute on <html> is also set to enable CSS-based layout switching.

Density Spacing

Three density levels scale all spacing tokens:

TokenCompactComfortableSpacious
--theme-space-xs0.2rem0.25rem0.375rem
--theme-space-sm0.35rem0.5rem0.625rem
--theme-space-md0.5rem0.75rem1rem
--theme-space-lg0.65rem1rem1.25rem
--theme-space-xl0.85rem1.25rem1.75rem
--theme-card-padding0.75rem1rem1.25rem
--theme-page-padding1rem1.5rem2rem

localStorage Persistence

The themeApi (src/theme/themeApi.ts) provides a mock API that uses localStorage:

typescript
interface ThemeConfigAPI {
  getTheme: () => Promise<ThemeConfig>    // Read with version check
  saveTheme: (config: ThemeConfig) => Promise<void>  // Write
  resetTheme: () => Promise<void>         // Delete saved config
}
  • Storage key: docurious-theme-config
  • Version key: docurious-theme-version (current: '3')
  • Version mismatches automatically clear old config and fall back to defaults
  • Saved configs are merged with defaults to ensure all keys exist

How to Create a New Preset

  1. Create src/theme/presets/myPreset.ts:
typescript
import type { ThemeConfig } from '../types'
import { defaultThemeConfig } from '../defaults'

export const myPresetConfig: ThemeConfig = {
  ...defaultThemeConfig,
  colors: {
    ...defaultThemeConfig.colors,
    primary: '#...',
    accent: '#...',
    // ... override colors
  },
  style: {
    ...defaultThemeConfig.style,
    cardBorderRadius: '1rem',
    density: 'spacious',
    // ... override style tokens
  },
}
  1. Register in src/theme/presets/index.ts:
typescript
import { myPresetConfig } from './myPreset'

// Add to the themePresets array:
{
  id: 'myPreset',
  name: 'My Preset',
  description: 'Description of the visual style',
  config: myPresetConfig,
  isDark: false,
  swatches: ['#primary', '#accent', '#background'],
},
  1. Export from src/theme/presets/index.ts:
typescript
export { myPresetConfig } from './myPreset'

Admin Theme Editor

The Design Token Editor at /admin/theme-editor (platform_admin only) provides a visual interface for editing all theme tokens in real time. Changes are persisted via themeApi.saveTheme().

DoCurious Platform Documentation