Skip to main content
The T1 Component Library includes a powerful theming system that provides seamless dark and light mode switching, comprehensive design tokens, and persistent user preferences.

Overview

The theme system is built with React Context and supports three modes:
  • Light mode: Bright, clean appearance optimized for daytime use
  • Dark mode: Low-light friendly with reduced eye strain
  • System mode: Automatically follows the user’s operating system preferences

Implementation

The theme system is implemented in ThemeContext.tsx using React Context API and provides automatic theme persistence via localStorage.

ThemeProvider

Wrap your application with the ThemeProvider to enable theming throughout your app:
import { ThemeProvider } from './context/ThemeContext';

function App() {
  return (
    <ThemeProvider>
      {/* Your app components */}
    </ThemeProvider>
  );
}

Using the Theme Hook

Access theme functionality anywhere in your app using the useTheme hook:
import { useTheme } from './context/ThemeContext';

function MyComponent() {
  const { theme, resolvedTheme, setTheme, toggleTheme, mounted } = useTheme();

  return (
    <div>
      <p>Current theme: {theme}</p>
      <p>Resolved theme: {resolvedTheme}</p>
      <button onClick={toggleTheme}>Toggle Theme</button>
    </div>
  );
}

Theme API

The useTheme hook returns the following properties and methods:
PropertyTypeDescription
theme'light' | 'dark' | 'system'The currently selected theme mode
resolvedTheme'light' | 'dark'The actual theme being displayed (resolves ‘system’ to ‘light’ or ‘dark’)
setTheme(theme: Theme) => voidManually set the theme mode
toggleTheme() => voidToggle between light and dark modes
mountedbooleanIndicates if the theme has been initialized (prevents flash of wrong theme)

How It Works

1

Initialization

On mount, the theme provider checks localStorage for a saved theme preference. If none exists, it defaults to ‘system’ mode.
2

Theme Resolution

For ‘system’ mode, the provider listens to the prefers-color-scheme media query to detect the OS preference. For explicit ‘light’ or ‘dark’ modes, it applies them directly.
3

DOM Application

The resolved theme is applied by adding or removing the ‘dark’ class on the document root element (<html>).
4

Persistence

Theme changes are automatically saved to localStorage, ensuring user preferences persist across sessions.

Design Tokens

The theme system uses CSS custom properties (variables) for consistent styling across components. All tokens are defined in globals.css and automatically update based on the active theme.

Color Tokens

Primary Colors

/* Light Mode */
--primary: #4f46e5;           /* Deep Indigo */
--primary-hover: #4338ca;
--primary-foreground: #ffffff;

/* Dark Mode */
--primary: #818cf8;           /* Lighter Indigo */
--primary-hover: #a5b4fc;
--primary-foreground: #0f0f1a;

Semantic Colors

TokenLight ModeDark ModeUsage
--accent#14b8a6 (Teal)#2dd4bfHighlights and CTAs
--success#10b981 (Emerald)#34d399Success states
--warning#f59e0b (Amber)#fbbf24Warnings
--destructive#f43f5e (Rose)#fb7185Errors and destructive actions

Surface Colors

/* Light Mode */
--background: #fafbfc;
--foreground: #1a1a2e;
--card: #ffffff;
--muted: #f8fafc;

/* Dark Mode */
--background: #0f0f1a;
--foreground: #f1f5f9;
--card: #1a1a2e;
--muted: #1e293b;

Spacing & Layout

Border Radius

--radius-sm: 0.375rem;   /* 6px */
--radius-md: 0.5rem;     /* 8px */
--radius-lg: 0.75rem;    /* 12px */
--radius-xl: 1rem;       /* 16px */
--radius-full: 9999px;   /* Fully rounded */

Shadows

--shadow-sm: 0 1px 2px 0 rgb(0 0 0 / 0.05);
--shadow-md: 0 4px 6px -1px rgb(0 0 0 / 0.1);
--shadow-lg: 0 10px 15px -3px rgb(0 0 0 / 0.1);
--shadow-xl: 0 20px 25px -5px rgb(0 0 0 / 0.1);
Shadow opacity is increased in dark mode (0.3-0.4) for better depth perception against dark backgrounds.

Transitions

--transition-fast: 150ms;
--transition-normal: 200ms;
--transition-slow: 300ms;

Using Design Tokens

Access design tokens in your CSS or components:

In CSS

.my-component {
  background-color: var(--card);
  color: var(--card-foreground);
  border-radius: var(--radius-md);
  box-shadow: var(--shadow-md);
  transition: all var(--transition-normal);
}

.my-component:hover {
  background-color: var(--primary);
  color: var(--primary-foreground);
}

In Tailwind CSS

Design tokens are mapped to Tailwind classes:
<div className="bg-card text-card-foreground rounded-md shadow-md">
  <button className="bg-primary text-primary-foreground hover:bg-primary-hover">
    Click me
  </button>
</div>

Advanced Features

System Theme Sync

When set to ‘system’ mode, the theme automatically updates when the user changes their OS preferences:
// From ThemeContext.tsx:51-57
if (theme === 'system') {
  const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
  applyTheme(mediaQuery.matches);

  const handler = (e: MediaQueryListEvent) => applyTheme(e.matches);
  mediaQuery.addEventListener('change', handler);
  return () => mediaQuery.removeEventListener('change', handler);
}

Preventing Theme Flash

Use the mounted property to prevent flash of unstyled content:
function ThemedComponent() {
  const { mounted, resolvedTheme } = useTheme();

  if (!mounted) {
    return <div className="skeleton" />; // Show skeleton while loading
  }

  return (
    <div className="animate-fade-in">
      Current theme: {resolvedTheme}
    </div>
  );
}

Example: Theme Switcher

Create a complete theme switcher component:
import { useTheme } from './context/ThemeContext';
import { Sun, Moon, Monitor } from 'lucide-react';

function ThemeSwitcher() {
  const { theme, setTheme } = useTheme();

  const themes = [
    { value: 'light', icon: Sun, label: 'Light' },
    { value: 'dark', icon: Moon, label: 'Dark' },
    { value: 'system', icon: Monitor, label: 'System' },
  ];

  return (
    <div className="flex gap-2">
      {themes.map(({ value, icon: Icon, label }) => (
        <button
          key={value}
          onClick={() => setTheme(value)}
          className={`
            p-2 rounded-md transition-colors
            ${theme === value 
              ? 'bg-primary text-primary-foreground' 
              : 'bg-secondary text-secondary-foreground hover:bg-secondary-hover'
            }
          `}
          aria-label={`Switch to ${label} theme`}
        >
          <Icon size={20} />
        </button>
      ))}
    </div>
  );
}

Best Practices

Always use design tokens instead of hardcoded colors to ensure your components automatically adapt to theme changes.
  1. Use semantic color tokens (--primary, --success, etc.) instead of specific colors
  2. Test components in both themes to ensure readability and accessibility
  3. Leverage the mounted flag to prevent flash of unstyled content
  4. Provide theme toggle UI in an accessible location (header, settings, etc.)
  5. Use CSS transitions for smooth theme switching animations

Source Code References

  • Theme Context: client/app/context/ThemeContext.tsx
  • Design Tokens: client/app/globals.css:4-122
  • Theme Hook: client/app/context/ThemeContext.tsx:84-87

Build docs developers (and LLMs) love