Files
alla-allaos-fullstack/docs/themes.md
phaichayon 7a390cf0df init
2026-06-11 12:46:57 +07:00

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:

  1. Create theme CSS filesrc/styles/themes/your-theme-name.css with [data-theme='your-theme-name']
  2. Import theme → Add @import to src/styles/theme.css
  3. Register theme → Add to THEMES array in src/components/themes/theme.config.ts
  4. Add fonts (if needed) → Import fonts in src/components/themes/font.config.ts if using custom Google Fonts
  5. Set as default (optional) → Update DEFAULT_THEME in src/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):

  1. Open src/components/themes/active-theme.tsx
  2. Change line 12: const DEFAULT_THEME = 'your-theme-name';
  3. 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 inline block 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

  1. Import the font from next/font/google:
import { Your_Font_Name } from 'next/font/google';
  1. 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
});
  1. Add it to the fontVariables export:
export const fontVariables = cn(
  // ... existing fonts
  fontYourName.variable
);
  1. 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.ts for it to be loaded by Next.js
  • Font variables from font.config.ts are automatically applied to the body via layout.tsx
  • You can use any Google Font available in next/font/google
  • Check existing fonts in font.config.ts before adding new ones - you might be able to reuse them

Example: The notebook theme uses Architects Daughter:

  • Imported in font.config.ts as Architects_Daughter
  • Used in notebook.css as '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

  1. Start your development server
  2. Open the theme selector in the UI
  3. Select your new theme
  4. Verify it works in both light and dark modes
  5. Test scaled variant by selecting "Your Theme Name (Scaled)"
  6. 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:

  1. src/styles/themes/your-theme-name.css - Create theme file with [data-theme] attribute
  2. src/styles/theme.css - Import your theme file
  3. src/components/themes/theme.config.ts - Add theme to THEMES array
  4. ⚠️ src/components/themes/font.config.ts - Add fonts only if needed
  5. ⚠️ 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-1 through --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

  1. Use descriptive theme names: Use kebab-case (e.g., ocean-blue, forest-green)
  2. Provide both light and dark modes: Always define both variants
  3. Test accessibility: Ensure sufficient contrast between foreground and background
  4. Keep tokens consistent: Use similar lightness/chroma values for related colors
  5. 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 inline block includes all color mappings
  • Ensure OKLCH format is correct (no typos)

Dark Mode Not Working

  • Verify .dark selector is correct: [data-theme='name'].dark
  • Check that dark mode tokens are defined
  • Ensure next-themes is 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_THEME is defined in theme.config.ts and imported everywhere it's needed
  • Server-side: Applied immediately in the HTML data-theme attribute (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

  1. Import the font in src/components/themes/font.config.ts:
import { Roboto, Roboto_Mono } from 'next/font/google';
  1. Configure the font with a CSS variable:
const fontRoboto = Roboto({
  subsets: ['latin'],
  weight: ['400', '500', '700'],
  variable: '--font-roboto'
});
  1. Add to fontVariables export:
export const fontVariables = cn(
  // ... existing fonts
  fontRoboto.variable
);
  1. 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.ts to 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