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

596 lines
19 KiB
Markdown

# 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 file**`src/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:
```css
/* 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):
```bash
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:
```css
/* 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`:
```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`:
```typescript
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`:
```typescript
import { Your_Font_Name } from 'next/font/google';
```
2. **Configure the font** with a CSS variable:
```typescript
const fontYourName = Your_Font_Name({
subsets: ['latin'],
weight: ['400', '500', '700'], // Adjust weights as needed
variable: '--font-your-name' // Optional: custom variable name
});
```
3. **Add it to the `fontVariables` export**:
```typescript
export const fontVariables = cn(
// ... existing fonts
fontYourName.variable
);
```
4. **Use the font in your theme CSS** by its display name (not the CSS variable):
```css
[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`
```typescript
/**
* 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
```css
/* 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:
```typescript
/**
* 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`:
```typescript
import { Roboto, Roboto_Mono } from 'next/font/google';
```
2. **Configure the font** with a CSS variable:
```typescript
const fontRoboto = Roboto({
subsets: ['latin'],
weight: ['400', '500', '700'],
variable: '--font-roboto'
});
```
3. **Add to fontVariables export**:
```typescript
export const fontVariables = cn(
// ... existing fonts
fontRoboto.variable
);
```
4. **Use in your theme CSS** with the font's display name:
```css
[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](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) for available Google Fonts
### Example: Notebook Theme
The `notebook` theme uses `Architects Daughter`:
**In `font.config.ts`:**
```typescript
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`:**
```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`