19 KiB
Adding New Themes
This guide explains how to add a new theme to the application. The theme system uses CSS custom properties with [data-theme] selectors for easy theme switching.
The Journey: Adding a New Theme
When adding a new theme, follow this journey:
- Create theme CSS file →
src/styles/themes/your-theme-name.csswith[data-theme='your-theme-name'] - Import theme → Add
@importtosrc/styles/theme.css - Register theme → Add to
THEMESarray insrc/components/themes/theme.config.ts - Add fonts (if needed) → Import fonts in
src/components/themes/font.config.tsif using custom Google Fonts - Set as default (optional) → Update
DEFAULT_THEMEinsrc/components/themes/active-theme.tsx
See the Step-by-Step Guide section below for detailed instructions.
Quick Start: Set Your Theme as Default
To make your new theme the default (so it loads automatically without the theme switcher):
- Open
src/components/themes/active-theme.tsx - Change line 12:
const DEFAULT_THEME = 'your-theme-name'; - Save and restart your dev server
That's it! Your theme will now be the default for all new users.
Note: Make sure you've completed steps 1-3 above before setting a theme as default.
Theme Structure
All themes are located in src/styles/themes/ directory. Each theme is a complete, self-contained CSS file that defines all design tokens for both light and dark modes.
File Format
Each theme file must follow this structure:
/* Light mode tokens */
[data-theme='your-theme-name'] {
/* Color tokens */
--background: oklch(...);
--foreground: oklch(...);
--card: oklch(...);
--card-foreground: oklch(...);
--popover: oklch(...);
--popover-foreground: oklch(...);
--primary: oklch(...);
--primary-foreground: oklch(...);
--secondary: oklch(...);
--secondary-foreground: oklch(...);
--muted: oklch(...);
--muted-foreground: oklch(...);
--accent: oklch(...);
--accent-foreground: oklch(...);
--destructive: oklch(...);
--destructive-foreground: oklch(...);
--border: oklch(...);
--input: oklch(...);
--ring: oklch(...);
/* Chart colors */
--chart-1: oklch(...);
--chart-2: oklch(...);
--chart-3: oklch(...);
--chart-4: oklch(...);
--chart-5: oklch(...);
/* Sidebar colors */
--sidebar: oklch(...);
--sidebar-foreground: oklch(...);
--sidebar-primary: oklch(...);
--sidebar-primary-foreground: oklch(...);
--sidebar-accent: oklch(...);
--sidebar-accent-foreground: oklch(...);
--sidebar-border: oklch(...);
--sidebar-ring: oklch(...);
/* Typography */
/* Option 1: Use fonts from next/font/google (recommended) */
--font-sans: 'Font Name', sans-serif; /* Use the font's display name */
--font-serif: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif;
--font-mono: 'Mono Font Name', monospace;
/* Option 2: Use system fonts */
/* --font-sans: ui-sans-serif, system-ui, -apple-system, sans-serif; */
/* Spacing & Layout */
--radius: 0.5rem;
--spacing: 0.25rem;
/* Shadows (optional) */
--shadow-x: 0px;
--shadow-y: 1px;
--shadow-blur: 3px;
--shadow-spread: 0px;
--shadow-opacity: 0.17;
--shadow-color: #000000;
--shadow-2xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
--shadow-xs: 0px 1px 3px 0px hsl(0 0% 0% / 0.09);
--shadow-sm: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
--shadow: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 1px 2px -1px hsl(0 0% 0% / 0.17);
--shadow-md: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 2px 4px -1px hsl(0 0% 0% / 0.17);
--shadow-lg: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 4px 6px -1px hsl(0 0% 0% / 0.17);
--shadow-xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.17), 0px 8px 10px -1px hsl(0 0% 0% / 0.17);
--shadow-2xl: 0px 1px 3px 0px hsl(0 0% 0% / 0.43);
/* Letter spacing (optional) */
--tracking-normal: 0em;
}
/* Dark mode tokens */
[data-theme='your-theme-name'].dark {
/* Same tokens as above, but with dark mode values */
--background: oklch(...);
--foreground: oklch(...);
/* ... all other tokens with dark mode values */
}
/* Theme inline mappings */
[data-theme='your-theme-name'] {
@theme inline {
/* Color mappings */
--color-background: var(--background);
--color-foreground: var(--foreground);
--color-card: var(--card);
--color-card-foreground: var(--card-foreground);
--color-popover: var(--popover);
--color-popover-foreground: var(--popover-foreground);
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--color-secondary: var(--secondary);
--color-secondary-foreground: var(--secondary-foreground);
--color-muted: var(--muted);
--color-muted-foreground: var(--muted-foreground);
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
--color-chart-1: var(--chart-1);
--color-chart-2: var(--chart-2);
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-sidebar: var(--sidebar);
--color-sidebar-foreground: var(--sidebar-foreground);
--color-sidebar-primary: var(--sidebar-primary);
--color-sidebar-primary-foreground: var(--sidebar-primary-foreground);
--color-sidebar-accent: var(--sidebar-accent);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-ring: var(--sidebar-ring);
/* Font mappings */
--font-sans: var(--font-sans);
--font-mono: var(--font-mono);
--font-serif: var(--font-serif);
/* Radius variants */
--radius-sm: calc(var(--radius) - 4px);
--radius-md: calc(var(--radius) - 2px);
--radius-lg: var(--radius);
--radius-xl: calc(var(--radius) + 4px);
/* Shadow mappings (if shadows are defined) */
--shadow-2xs: var(--shadow-2xs);
--shadow-xs: var(--shadow-xs);
--shadow-sm: var(--shadow-sm);
--shadow: var(--shadow);
--shadow-md: var(--shadow-md);
--shadow-lg: var(--shadow-lg);
--shadow-xl: var(--shadow-xl);
--shadow-2xl: var(--shadow-2xl);
/* Tracking variants (if tracking-normal is defined) */
--tracking-tighter: calc(var(--tracking-normal) - 0.05em);
--tracking-tight: calc(var(--tracking-normal) - 0.025em);
--tracking-normal: var(--tracking-normal);
--tracking-wide: calc(var(--tracking-normal) + 0.025em);
--tracking-wider: calc(var(--tracking-normal) + 0.05em);
--tracking-widest: calc(var(--tracking-normal) + 0.1em);
}
}
Step-by-Step Guide: Adding a New Theme
Follow these steps in order to add a new theme to your application.
Step 1: Create Theme CSS File
Create a new file in src/styles/themes/ with a descriptive name (use kebab-case):
src/styles/themes/your-theme-name.css
Important: The filename should match the data-theme attribute value you'll use in the CSS.
Step 2: Define Your Theme with [data-theme] Attribute
Copy the structure from the "File Format" section above and fill in your color values. Use OKLCH color format for better color consistency:
/* Light mode tokens */
[data-theme='your-theme-name'] {
--background: oklch(1 0 0); /* White */
--foreground: oklch(0.145 0 0); /* Dark gray */
--card: oklch(...);
/* ... all other tokens */
}
/* Dark mode tokens */
[data-theme='your-theme-name'].dark {
--background: oklch(0.145 0 0); /* Dark */
--foreground: oklch(0.985 0 0); /* Light */
/* ... all other tokens with dark mode values */
}
/* Theme inline mappings for Tailwind */
[data-theme='your-theme-name'] {
@theme inline {
/* All the mappings as shown in the File Format section */
}
}
Color Format:
- Use
oklch()format:oklch(lightness chroma hue) - Example:
oklch(0.852 0.199 91.936)= light green-blue - Lightness: 0-1 (0 = black, 1 = white)
- Chroma: 0+ (0 = grayscale, higher = more saturated)
- Hue: 0-360 (color wheel position)
Key Points:
- The
[data-theme='your-theme-name']selector is what makes your theme work - The value
'your-theme-name'must match exactly in all places (CSS file, config, etc.) - Always include both light and dark mode variants
- Include the
@theme inlineblock for Tailwind CSS integration
Step 3: Import Theme in theme.css
Add your theme import to src/styles/theme.css:
@import './themes/your-theme-name.css';
This makes your theme available to the application.
Step 4: Add Theme to theme.config.ts
Add your theme to the THEMES array in src/components/themes/theme.config.ts:
export const THEMES = [
// ... existing themes
{
name: 'Your Theme Name', // Display name in the UI
value: 'your-theme-name' // Must match [data-theme] value exactly
}
];
Important: The value field must match the data-theme attribute value from your CSS file exactly.
Step 5: Add Custom Fonts (If Needed)
Only do this step if your theme requires a custom Google Font that isn't already loaded.
If you want to use a Google Font in your theme:
File: src/components/themes/font.config.ts
- Import the font from
next/font/google:
import { Your_Font_Name } from 'next/font/google';
- Configure the font with a CSS variable:
const fontYourName = Your_Font_Name({
subsets: ['latin'],
weight: ['400', '500', '700'], // Adjust weights as needed
variable: '--font-your-name' // Optional: custom variable name
});
- Add it to the
fontVariablesexport:
export const fontVariables = cn(
// ... existing fonts
fontYourName.variable
);
- Use the font in your theme CSS by its display name (not the CSS variable):
[data-theme='your-theme-name'] {
--font-sans: 'Your Font Name', sans-serif; /* Use the actual font name */
--font-mono: 'Your Mono Font', monospace;
}
Important Notes:
- Use the font's display name in CSS (e.g.,
'Geist','Architects Daughter'), not the CSS variable - The font must be imported in
font.config.tsfor it to be loaded by Next.js - Font variables from
font.config.tsare automatically applied to the body vialayout.tsx - You can use any Google Font available in
next/font/google - Check existing fonts in
font.config.tsbefore adding new ones - you might be able to reuse them
Example: The notebook theme uses Architects Daughter:
- Imported in
font.config.tsasArchitects_Daughter - Used in
notebook.cssas'Architects Daughter'(with quotes and space)
Step 6: Set as Default Theme (Optional)
If you want your theme to be the default theme that loads when users first visit the application (without needing the theme switcher), update the default theme constant:
File: src/components/themes/theme.config.ts
/**
* Default theme that loads when no user preference is set
* Change this value to set a different default theme
*/
export const DEFAULT_THEME = 'your-theme-name'; // Change from 'vercel' to your theme name
Note:
- This is the single source of truth for the default theme - it's automatically used in both server-side rendering and client-side code
- This will make your theme the default for all new users
- Existing users who have already selected a theme will still see their saved preference (stored in cookies)
- The default theme is applied immediately on page load (no flash of unstyled content)
Step 7: Test Your Theme
- Start your development server
- Open the theme selector in the UI
- Select your new theme
- Verify it works in both light and dark modes
- Test scaled variant by selecting "Your Theme Name (Scaled)"
- If you set it as default, clear your browser cookies and refresh to see it load automatically
Quick Reference: File Locations
When adding a new theme, you'll work with these files in this order:
- ✅
src/styles/themes/your-theme-name.css- Create theme file with[data-theme]attribute - ✅
src/styles/theme.css- Import your theme file - ✅
src/components/themes/theme.config.ts- Add theme toTHEMESarray - ⚠️
src/components/themes/font.config.ts- Add fonts only if needed - ⚠️
src/components/themes/active-theme.tsx- Set as default only if desired
Required Tokens
Minimum Required
At minimum, your theme should define these tokens:
--background--foreground--card&--card-foreground--popover&--popover-foreground--primary&--primary-foreground--secondary&--secondary-foreground--muted&--muted-foreground--accent&--accent-foreground--destructive&--destructive-foreground--border--input--ring--radius
Optional Tokens
These can be omitted if not needed:
--chart-1through--chart-5(defaults to primary colors)--sidebar-*tokens (defaults to card colors)--font-*tokens (uses system defaults)--shadow-*tokens (no shadows if omitted)--tracking-normal(no letter spacing if omitted)--spacing(uses default)
Example: Complete Theme
See src/styles/themes/claude.css for a complete example with all tokens defined.
Example: Minimal Theme
For a minimal theme, you can copy an existing theme and modify only the colors you want to change. The system will fall back to defaults for any missing tokens.
Color Format Reference
OKLCH Format
oklch(lightness chroma hue)
- Lightness: 0-1 (0 = black, 1 = white)
- Chroma: 0+ (0 = grayscale, 0.2+ = colorful)
- Hue: 0-360 degrees
- 0/360 = Red
- 60 = Yellow
- 120 = Green
- 180 = Cyan
- 240 = Blue
- 300 = Magenta
Examples
/* Pure white */
--background: oklch(1 0 0);
/* Pure black */
--foreground: oklch(0 0 0);
/* Bright blue */
--primary: oklch(0.7 0.2 240);
/* Muted gray */
--muted: oklch(0.5 0 0);
Scaled Variants
All themes automatically support scaled variants. When a user selects "Theme Name (Scaled)", the .theme-scaled class is applied, which adjusts spacing and text sizes. No additional CSS is needed in your theme file.
Best Practices
- Use descriptive theme names: Use kebab-case (e.g.,
ocean-blue,forest-green) - Provide both light and dark modes: Always define both variants
- Test accessibility: Ensure sufficient contrast between foreground and background
- Keep tokens consistent: Use similar lightness/chroma values for related colors
- Document special features: If your theme has unique characteristics (like no shadows or custom fonts), add comments
Troubleshooting
Theme Not Appearing
- Check that the file is imported in
src/styles/theme.css - Verify the theme name matches in both CSS file and theme-selector.tsx
- Ensure the file is saved and the dev server has reloaded
Colors Not Applying
- Verify all required tokens are defined
- Check that
@theme inlineblock includes all color mappings - Ensure OKLCH format is correct (no typos)
Dark Mode Not Working
- Verify
.darkselector is correct:[data-theme='name'].dark - Check that dark mode tokens are defined
- Ensure
next-themesis properly configured
Setting a Default Theme
By default, the application uses the vercel theme. To change the default theme that loads for new users:
Change Default Theme Constant
Edit src/components/themes/theme.config.ts and update the DEFAULT_THEME constant:
/**
* Default theme that loads when no user preference is set
* Change this value to set a different default theme
*/
export const DEFAULT_THEME = 'your-theme-name'; // Change this value
How it works:
- Single source of truth:
DEFAULT_THEMEis defined intheme.config.tsand imported everywhere it's needed - Server-side: Applied immediately in the HTML
data-themeattribute (no flash) - Client-side: Used as fallback when no cookie preference exists
- User preferences: Still respects saved user preferences (stored in cookies)
- Automatic: No need to update multiple files - change it once and it works everywhere
Benefits of this approach:
✅ No code duplication - defined once, used everywhere
✅ Type-safe - TypeScript ensures consistency
✅ Easy to change - update one line in one file
✅ Well-documented - clear comments explain its purpose
✅ Immediate application - no flash of unstyled content
Using Google Fonts in Themes
Note: This section provides additional details about fonts. For the complete step-by-step process, see Step 5 in the "Step-by-Step Guide" above.
When to Add Fonts
You only need to add fonts to font.config.ts if:
- Your theme uses a Google Font that isn't already imported
- You want to use a custom font that requires loading
Tip: Check src/components/themes/font.config.ts first - many fonts may already be available!
Font Loading Process
- Import the font in
src/components/themes/font.config.ts:
import { Roboto, Roboto_Mono } from 'next/font/google';
- Configure the font with a CSS variable:
const fontRoboto = Roboto({
subsets: ['latin'],
weight: ['400', '500', '700'],
variable: '--font-roboto'
});
- Add to fontVariables export:
export const fontVariables = cn(
// ... existing fonts
fontRoboto.variable
);
- Use in your theme CSS with the font's display name:
[data-theme='your-theme'] {
--font-sans: 'Roboto', sans-serif; /* Use display name, not CSS variable */
--font-mono: 'Roboto Mono', monospace;
}
Important Notes
- Font names: Use the font's display name in CSS (e.g.,
'Roboto','Open Sans'), not the CSS variable name - Font loading: Fonts must be imported in
font.config.tsto be loaded by Next.js - Automatic application: Font variables are automatically applied to the body element via
layout.tsx - Available fonts: Check Next.js Font Optimization for available Google Fonts
Example: Notebook Theme
The notebook theme uses Architects Daughter:
In font.config.ts:
import { Architects_Daughter } from 'next/font/google';
const fontArchitectsDaughter = Architects_Daughter({
subsets: ['latin'],
weight: '400',
variable: '--font-architects-daughter'
});
export const fontVariables = cn(
// ... other fonts
fontArchitectsDaughter.variable
);
In notebook.css:
[data-theme='notebook'] {
--font-sans: 'Architects Daughter', sans-serif;
}
Reference Files
- Complete theme example:
src/styles/themes/claude.css - Theme aggregator:
src/styles/theme.css - Theme selector component:
src/components/themes/theme-selector.tsx - Theme provider:
src/components/themes/active-theme.tsx - Theme configuration (includes default theme):
src/components/themes/theme.config.ts - Font configuration:
src/components/themes/font.config.ts