- Docs
- Design
- Themes & tokens
Themes & tokens
Custom Theme Guide
How to create a branded DaisyUI theme for your fork of ScriptHammer.
Quick Start
After running scripts/rebrand.sh, your src/app/globals.css will contain placeholder theme blocks named yourproject-dark and yourproject-light. Edit the oklch color values to match your brand.
Theme Syntax
ScriptHammer uses DaisyUI v5 with Tailwind v4's CSS-first configuration. Custom themes are defined using @plugin "daisyui/theme" blocks in src/app/globals.css.
Minimal Example
@plugin "daisyui/theme" {
name: 'myproject-dark';
default: true;
prefersdark: true;
color-scheme: dark;
--color-base-100: oklch(22.84% 0.038 282.93);
--color-base-200: oklch(21.13% 0.039 282.53);
--color-base-300: oklch(28.51% 0.067 281.32);
--color-base-content: oklch(92.88% 0.013 255.51);
--color-primary: oklch(76.05% 0.024 258.37);
--color-primary-content: oklch(22.84% 0.038 282.93);
--color-secondary: oklch(87.91% 0.043 76.31);
--color-secondary-content: oklch(22.84% 0.038 282.93);
--color-accent: oklch(75.35% 0.139 232.66);
--color-accent-content: oklch(22.84% 0.038 282.93);
--color-neutral: oklch(31.14% 0.052 282.99);
--color-neutral-content: oklch(87.17% 0.009 258.34);
--color-info: oklch(62.31% 0.188 259.81);
--color-info-content: oklch(96% 0.018 272.31);
--color-success: oklch(72.27% 0.192 149.58);
--color-success-content: oklch(22.84% 0.038 282.93);
--color-warning: oklch(90.52% 0.166 98.11);
--color-warning-content: oklch(22.84% 0.038 282.93);
--color-error: oklch(63.68% 0.208 25.33);
--color-error-content: oklch(96% 0.018 272.31);
--radius-selector: 0.5rem;
--radius-field: 0.25rem;
--radius-box: 0.75rem;
--size-selector: 0.25rem;
--size-field: 0.25rem;
--border: 1px;
--depth: 1;
--noise: 0;
}
Required Properties
| Property | Purpose | Dark theme guidance | Light theme guidance |
|---|---|---|---|
--color-base-100 |
Primary surface | Dark background (e.g., near-black) | Light background (e.g., off-white) |
--color-base-200 |
Secondary surface | Slightly darker than base-100 | Slightly darker than base-100 |
--color-base-300 |
Borders/dividers | Lighter than base-100 | Darker than base-100 |
--color-base-content |
Default text | Light text on dark surface | Dark text on light surface |
--color-primary |
Brand identity color | Your primary brand color | Darker variant for light bg contrast |
--color-primary-content |
Text on primary | Contrasts against primary | Contrasts against primary |
--color-secondary |
Supporting brand color | Your secondary brand color | Darker variant for light bg |
--color-secondary-content |
Text on secondary | Contrasts against secondary | Contrasts against secondary |
--color-accent |
Call-to-action/links | Bright, attention-grabbing | Darker variant for readability |
--color-accent-content |
Text on accent | Contrasts against accent | Contrasts against accent |
--color-neutral |
Muted UI elements | Dark muted tone | Dark muted tone |
--color-neutral-content |
Text on neutral | Light text | Light text |
--color-info |
Info alerts | Blue-ish | Darker blue |
--color-success |
Success states | Green-ish | Darker green |
--color-warning |
Warnings | Yellow-ish | Darker yellow |
--color-error |
Errors | Red-ish | Darker red |
Each semantic color has a -content variant for text rendered on top of that color.
Shape and Effect Properties
| Property | Default | Purpose |
|---|---|---|
--radius-selector |
0.5rem |
Border radius for toggle/checkbox/radio |
--radius-field |
0.25rem |
Border radius for inputs |
--radius-box |
0.75rem |
Border radius for cards/modals |
--size-selector |
0.25rem |
Size of toggle/checkbox indicators |
--size-field |
0.25rem |
Padding inside input fields |
--border |
1px |
Default border width |
--depth |
1 |
Shadow intensity (0 = flat) |
--noise |
0 |
Background noise texture (0 = none) |
Color Format: oklch
DaisyUI v5 uses oklch (Oklab Lightness Chroma Hue) color values. To convert hex to oklch:
// Run in Node.js or browser console
function hexToOklch(hex) {
const r = parseInt(hex.slice(1, 3), 16) / 255;
const g = parseInt(hex.slice(3, 5), 16) / 255;
const b = parseInt(hex.slice(5, 7), 16) / 255;
// sRGB to linear
const toLinear = (c) =>
c <= 0.04045 ? c / 12.92 : Math.pow((c + 0.055) / 1.055, 2.4);
const lr = toLinear(r),
lg = toLinear(g),
lb = toLinear(b);
// Linear RGB to Oklab
const l_ = Math.cbrt(
0.4122214708 * lr + 0.5363325363 * lg + 0.0514459929 * lb
);
const m_ = Math.cbrt(
0.2119034982 * lr + 0.6806995451 * lg + 0.1073969566 * lb
);
const s_ = Math.cbrt(
0.0883024619 * lr + 0.2817188376 * lg + 0.6299787005 * lb
);
const L = 0.2104542553 * l_ + 0.793617785 * m_ - 0.0040720468 * s_;
const a = 1.9779984951 * l_ - 2.428592205 * m_ + 0.4505937099 * s_;
const bv = 0.0259040371 * l_ + 0.7827717662 * m_ - 0.808675766 * s_;
const C = Math.sqrt(a * a + bv * bv);
const H = ((Math.atan2(bv, a) * 180) / Math.PI + 360) % 360;
return `oklch(${(L * 100).toFixed(2)}% ${C.toFixed(3)} ${H.toFixed(2)})`;
}
// Example: hexToOklch('#1a1a2e') → 'oklch(22.84% 0.038 282.93)'
Alternatively, use the browser DevTools color picker which shows oklch values directly.
Registration
After defining your theme blocks, register them in the @plugin "daisyui" block at the top of globals.css:
@plugin "daisyui" {
themes:
myproject-dark --default --prefersdark,
myproject-light,
light,
dark,
/* ... other DaisyUI built-in themes */;
}
--defaultmarks the theme loaded on first visit--prefersdarkmarks the theme used when the user's OS prefers dark mode
Other Files to Update
ThemeScript.tsx
Update the system theme fallback in src/components/ThemeScript.tsx:
// Change these two return values:
return 'myproject-dark'; // was 'scripthammer-dark'
return 'myproject-light'; // was 'scripthammer-light'
Storybook Preview
In .storybook/preview.tsx, add your themes to the switcher and set the default:
themes: {
'myproject-dark': 'myproject-dark',
'myproject-light': 'myproject-light',
// ... other themes
},
defaultTheme: 'myproject-dark',
PWA Manifest
In src/config/project.config.ts, update theme and background colors to match your dark theme's base-100:
theme_color: '#your-base-100-hex',
background_color: '#your-base-100-hex',
Map Controls (if using Leaflet/MapLibre)
If your project includes map features, add your dark theme to the Leaflet control selectors in globals.css:
[data-theme='myproject-dark'] .leaflet-control,
[data-theme='dark'] .leaflet-control,
/* ... */
WCAG AA Contrast Requirements
All text must meet WCAG 2.1 AA minimum contrast ratios:
| Element | Ratio | How to check |
|---|---|---|
| Normal text (< 18px) | 4.5:1 | base-content against base-100 |
| Large text (>= 18px bold) | 3:1 | Headings against surfaces |
| UI components | 3:1 | Borders, icons, form controls |
primary-content on primary |
4.5:1 | Button text |
secondary-content on secondary |
4.5:1 | Badge text |
accent-content on accent |
4.5:1 | Link hover states |
Use WebAIM Contrast Checker or Storybook's a11y addon to verify.
Verification
After defining your themes:
# Type-check (catches syntax errors in CSS)
docker compose exec <project> pnpm run type-check
# Storybook build (renders all components with your theme)
docker compose exec <project> pnpm run build-storybook
# Visual check in Storybook
docker compose exec <project> pnpm run storybook
# Switch themes using the toolbar dropdown