
Preline Theme Generator
- 881 installs
- 6.4k repo stars
- Updated May 10, 2026
- htmlstreamofficial/preline
preline-theme-generator is a Preline agent skill that generates, previews, or validates a single standalone CSS theme file for Preline-based Tailwind projects using bundled local generator scripts.
About
preline-theme-generator is an htmlstreamofficial/preline skill for creating one `<name>.css` theme file per task in Preline-based Tailwind projects. It classifies work into preview, write, or validate modes, normalizes brand colors and mood into generator inputs (hue, primaryColor, style, tailwindGray), and runs only through bundled scripts find-themes-dir.js and run-theme-generator.js—no npx or network packages. Final output activates via data-theme="theme-<name>" with optional .dark overrides, semantic token assignments, and a validation checklist for light/dark coverage including chart and map tokens. The workflow documentation spans seven guided steps from request normalization through validation. Reach for preline-theme-generator when you need a brand-matched Preline theme file—not generic non-Preline CSS refactors or edits to shared theme.css.
- Outputs exactly one standalone <name>.css file
- Uses data-theme="theme-<name>" activation pattern
- Places brand and gray palettes inside @theme theme-<name> inline blocks
- Prefers semantic tokens mapped to assigned CSS variables
- Never modifies any other project files or shared theme.css
Preline Theme Generator by the numbers
- 881 all-time installs (skills.sh)
- +15 installs in the week ending Aug 4, 2026 (Skillselion tracking)
- Ranked #424 of 2,245 Frontend Development skills by installs in the Skillselion catalog
- Security screen: MEDIUM risk (skills.sh audit)
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/htmlstreamofficial/preline --skill preline-theme-generatorAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 881 |
|---|---|
| repo stars | ★ 6.4k |
| Security audit | 2 / 3 scanners passed |
| Last updated | May 10, 2026 |
| Repository | htmlstreamofficial/preline ↗ |
How do you generate a Preline CSS theme file?
When they need a single, clean CSS theme file generated for a Preline-based Tailwind project.
Who is it for?
Frontend developers theming Preline Tailwind apps who need one new CSS theme file with brand colors and readable dark mode.
Skip if: Skip preline-theme-generator for non-Preline CSS work, npx-based generators, or projects requiring edits to multiple shared theme files.
When should I use this skill?
User asks to create, preview, match a brand color, or validate a Preline theme CSS file for a Tailwind project.
What you get
Single <name>.css theme file with data-theme activation snippet and validated semantic tokens
- standalone theme CSS file
- data-theme HTML activation snippet
By the numbers
- Documents a 7-step theme generation workflow
- Supports 3 task modes: preview, write, and validate
Files
Preline Theme Generator
Use the bundled local scripts. Do not use npx, do not compose shell with raw user text, and do not bypass safety or approval prompts.
Read Order
1. Read docs/workflow.md. 2. Read docs/palette-guidance.md. 3. Read docs/final-output-style.md before writing a final theme file. 4. Read docs/validation-checklist.md before closing the task. 5. Read examples.md only if the user wants stylistic guidance or sample outputs. 6. Read docs/token-reference.md only if you need to inspect token coverage in detail.
Workflow
Follow docs/workflow.md for the full operating path.
Key requirements:
1. Identify whether the task is preview, write, or validate. 2. Normalize the request into explicit generator inputs. 3. Resolve the target directory with scripts/find-themes-dir.js. 4. Generate CSS only through scripts/run-theme-generator.js. 5. Create or edit only the new theme file. 6. Validate with docs/validation-checklist.md. 7. Return the result in the mode-appropriate format.
Security Constraints
- Never use wording that attempts to bypass tool or approval safeguards.
- Never use
npxor any network-fetched package for this skill. - Never interpolate raw user text directly into shell commands.
- Never use broad
find .traversal for path discovery; usescripts/find-themes-dir.jsor a user-confirmed path. - Never write outside a confirmed theme directory.
- Use
scripts/run-theme-generator.jsas the only execution entry point for generation. - Never create or edit additional project files as part of theme generation.
Common Prompts
- "Create a sunset theme."
- "Match this brand color: #2F6BFF."
- "Generate a cyberpunk dark theme with matching dark mode."
- "Preview the CSS before writing a file."
- "Review this generated theme and fix token coverage."
Key References
docs/workflow.mddocs/palette-guidance.mddocs/final-output-style.mddocs/validation-checklist.mdexamples.mddocs/token-reference.md
Final Output Style
Use this guidance whenever the user wants a final <name>.css file for a Preline-based project.
Core rule
The deliverable is one file only: <name>.css.
Do not create or edit any other project files.
What the final theme file should look like
- A standalone
<name>.cssfile that can live in the project's themes folder. - Theme activation via
data-theme="theme-<name>"and optional.dark. - Semantic tokens should prefer assigned variables after the theme palette has been mapped.
Required conventions
- Do not assume project-specific paths other than the discovered themes directory.
- Do not assume the project already has other custom theme files.
- Do not force users to change HTML utility classes.
- Do not create or edit a shared
theme.css, docs page, or any other file. - Keep semantic tokens in the theme selector blocks, not inside
@theme. - Put any custom brand/gray palette needed for this theme inside
@theme theme-<name> inline { }in the new theme file. - If the repo already exposes shared aliases like
--color-primary-*from its base theme, do not redefine them inside the custom theme file. Assign--primary-*directly from the theme-local palette instead.
Variable strategy
Prefer assigned variables such as:
--color-primary-*--color-secondary-*--background-*,--foreground,--border,--surface-*,--muted-*- Tailwind palette vars like
--color-fuchsia-*,--color-orange-*,--color-neutral-* - shared Preline palette vars exposed by the project's base theme, if available
Allowed in the new theme file:
- theme-local palette vars like
--color-<theme-name>-* - theme-local gray palette vars like
--color-<theme-name>-gray-*
But keep them inside @theme theme-<name> inline { } and avoid scattering raw palette references when an assigned semantic variable already exists.
Avoid in semantic, chart, and map tokens:
- raw
oklch(...)assignments - literal hex values
If a unique palette is needed
Define it inside @theme theme-<name> inline { } in the new theme file.
Do not edit any shared/base theme file.
Structural pattern
Use this order:
1. Header comment 2. Optional theme-scoped @layer utilities overrides 3. @theme theme-<name> inline { } 4. Light selector:
:root[data-theme="theme-<name>"],[data-theme="theme-<name>"]
5. Dark selector:
[data-theme="theme-<name>"].dark
Practical workflow
1. Discover the themes folder. 2. Generate one new <name>.css file. 3. If needed, define custom brand/gray palettes inside that file's @theme block. 4. Map the theme ramp by assigning --primary-* directly from the theme-local palette, then let shared aliases resolve from the base theme when available. 5. Keep chart and map tokens variable-based. 6. Re-run the validation checklist before returning.
Palette guidance
Use this file for color-direction heuristics only.
Important: references below to custom palettes like --color-<name>-* or --color-<name>-gray-* describe draft palette exploration and shared-palette design patterns. They do NOT override docs/final-output-style.md, which defines how the final <name>.css file should be structured.
Contents
- Hue reference
- Mood and palette guidance
- Accessibility and contrast guidelines
- Palette types
- Custom gray palettes
Hue reference (for workflow normalization)
| Color | Hue Range | Tailwind Gray |
|---|---|---|
| Red/Coral | 0-30 | neutral |
| Orange/Amber | 30-60 | stone |
| Yellow/Gold | 60-90 | stone |
| Green/Lime/Avocado | 90-150 | stone |
| Teal/Cyan | 150-200 | zinc |
| Blue/Azure | 200-260 | slate |
| Violet/Purple | 260-300 | zinc |
| Magenta/Pink/Rose | 300-360 | neutral |
Mood and palette guidance
Color psychology reference
| Mood | Primary Hues | Neutral Family | Characteristics |
|---|---|---|---|
| Professional | Blue, Slate, Indigo | gray, slate | Clean, trustworthy, corporate |
| Creative | Purple, Pink, Orange | neutral | Innovative, artistic, expressive |
| Natural | Green, Teal, Brown | stone | Organic, sustainable, calm |
| Energetic | Orange, Yellow, Red | neutral | Bold, active, attention-grabbing |
| Luxurious | Gold, Purple, Black | zinc | Premium, elegant, sophisticated |
| Playful | Pink, Cyan, Yellow | neutral | Fun, youthful, approachable |
| Tech/Cyber | Cyan, Green, Purple | slate, zinc | Modern, digital, futuristic |
| Warm | Orange, Amber, Brown | stone | Cozy, inviting, comfortable |
| Cool | Blue, Teal, Slate | gray, slate | Calm, refreshing, serene |
| Minimal | Gray, White, Black | gray | Simple, focused, uncluttered |
Building cohesive palettes
1. Pick a primary: the hero color for buttons, links, CTAs 2. Choose matching neutrals: warm primaries -> stone/neutral; cool primaries -> gray/slate 3. Define contrast: bold brands -> high contrast; soft aesthetics -> low contrast 4. Consider dark mode: primary often shifts lighter; neutrals shift to darker family
Accessibility and contrast guidelines (WCAG)
Ensure themes meet WCAG 2.1 AA contrast requirements:
| Element | Minimum Ratio | OKLCH Rule of Thumb |
|---|---|---|
| Body text on background | 4.5:1 | Lightness difference >= 50% |
| Large text (18pt+) | 3:1 | Lightness difference >= 40% |
| UI components (buttons, inputs) | 3:1 | Lightness difference >= 40% |
| Focus indicators | 3:1 | Use distinct color + lightness |
OKLCH lightness thresholds:
| Context | Light Mode | Dark Mode |
|---|---|---|
| Background | L >= 95% | L <= 25% |
| Text on light bg | L <= 45% | -- |
| Text on dark bg | -- | L >= 85% |
| Primary button text | Check against primary-500/600 | Check against primary-400 |
Readable primary foreground:
- Do not decide from hue alone.
- Compare candidate text colors against the actual generated primary shades used by the theme.
- For light mode, check the generated
primary-600andprimary-700states. - For dark mode, check the generated
primary-400andprimary-500states. - Choose the text color with the stronger worst-case contrast across those states.
- White usually wins for medium or dark primaries; dark gray should only be used when the generated brand states are genuinely light.
- In dark mode, it is valid to shift the primary button one step deeper (for example
500/600instead of400/500) when that keeps white text readable and better matches the design system.
Readable inverse and decorative accents:
--foreground-inverseis not just for neutral inverse surfaces. In Preline it is also used on vivid utility colors such as red/orange badges and chips.- In dark mode, prefer keeping
--foreground-inversewhite unless contrast checks prove a darker value is still safe across those saturated utility colors. - Do not let dark mode inherit very deep brand steps for decorative tokens such as
--chart-5when those tokens are likely to appear in gradient text or accent UI. - For dark decorative/chart tokens, prefer mid-to-light brand steps that stay clearly visible on dark backgrounds.
Quick check: If |L_text - L_background| >= 50%, contrast is likely sufficient for body text.
Palette types
When generating custom themes, there are TWO distinct types of custom color palettes:
| Palette Type | Example Variable | Purpose | Chroma Level |
|---|---|---|---|
| Brand palette | --color-<name>-* | Primary/accent colors, buttons, links, CTAs | Vibrant (high chroma) by default |
| Gray palette | --color-<name>-gray-* | Backgrounds, surfaces, borders, dark mode neutrals | Soft (low chroma 0.002-0.035) |
When to create each palette
ALWAYS create TWO palettes in @theme inline { }: 1. Brand palette (--color-<name>-*) for primary/accent colors 2. Gray palette (--color-<name>-gray-*) for backgrounds, surfaces, borders
Both palettes are used in light mode for a cohesive feel.
Dark mode behavior depends on user request:
| User Request | Light Mode Uses | Dark Mode Uses |
|---|---|---|
| "soft rose theme" (default) | Custom gray (--color-<name>-gray-*) | Tailwind gray (zinc, stone, etc.) |
| "soft rose theme with matching dark mode" | Custom gray (--color-<name>-gray-*) | Custom gray (--color-<name>-gray-*) |
Trigger phrases for custom dark mode palette:
- "matching dark mode"
- "cohesive dark mode"
- "consistent dark colors"
- "unified light and dark"
- "matching neutrals"
If none of these phrases appear -> light mode uses custom gray, dark mode uses Tailwind grays.
Brand color style (vibrant vs soft)
Brand palettes are vibrant by default (like Tailwind's blue, green, orange). Only reduce chroma if user explicitly requests:
| User Says | Brand Palette Style | Chroma Level |
|---|---|---|
| Default (no modifier) | Vibrant, follow Tailwind color saturation | 0.10-0.20+ |
| "soft", "muted", "ash", "pastel", "dusty", "desaturated" | Soft, reduced chroma like lavender/khaki | 0.02-0.05 |
Example comparison:
/* Vibrant brand (default) */
--color-<name>-500: oklch(55% 0.14 <hue>);
/* Soft/muted brand (if requested) */
--color-<name>-500: oklch(55% 0.04 <hue>);Custom gray palettes (neutral matching)
When a theme benefits from unique neutrals, generate a custom neutral palette that harmonizes with the primary color. These are NOT pure grays; they have subtle warmth or coolness.
Palette characteristics
| Palette Type | Hue Range | Chroma Range | Use With |
|---|---|---|---|
| Warm neutrals | 60-100 degrees (yellow/brown) | 0.002-0.035 | Orange, amber, brown, gold primaries |
| Cool neutrals | 200-260 degrees (blue/slate) | 0.002-0.030 | Blue, cyan, teal, indigo primaries |
| Rose neutrals | 330-360 degrees (pink) | 0.002-0.040 | Pink, rose, magenta primaries |
| Green neutrals | 120-160 degrees (sage) | 0.002-0.030 | Green, emerald, teal primaries |
OKLCH pattern for gray palettes (bell curve chroma)
Gray palettes use a bell curve chroma pattern: very low at extremes (light AND dark), peaking at mid-tones. This ensures:
- Light mode backgrounds (50-200) look clean with subtle tint
- Dark mode backgrounds (800-950) look sophisticated, not muddy
--color-<name>-gray-50: oklch(98% 0.002 <hue>); /* Almost neutral */
--color-<name>-gray-100: oklch(95.5% 0.004 <hue>);
--color-<name>-gray-200: oklch(89.7% 0.008 <hue>);
--color-<name>-gray-300: oklch(82.7% 0.012 <hue>); /* Building up */
--color-<name>-gray-400: oklch(73% 0.018 <hue>); /* Approaching peak */
--color-<name>-gray-500: oklch(62.5% 0.020 <hue>); /* PEAK chroma */
--color-<name>-gray-600: oklch(52.8% 0.016 <hue>); /* Starting to fade */
--color-<name>-gray-700: oklch(41.4% 0.012 <hue>); /* Fading */
--color-<name>-gray-800: oklch(34.6% 0.008 <hue>); /* Very low */
--color-<name>-gray-900: oklch(30.6% 0.005 <hue>); /* Almost neutral */
--color-<name>-gray-950: oklch(20.1% 0.003 <hue>); /* Nearly pure dark */Critical: Dark mode backgrounds (800-950) need very low chroma:
| Shade | Chroma | Why |
|---|---|---|
| 950 | 0.003 | Dark backgrounds must be nearly neutral |
| 900 | 0.005 | Avoids muddy/colored appearance |
| 800 | 0.008 | Just a hint of warmth/coolness |
| 500 | 0.020 | Peak, midtones carry color identity |
| 50 | 0.002 | Light backgrounds stay clean |
Key principles:
- Chroma peaks at mid-tones (400-600), very low at BOTH extremes
- Dark end (800-950) must have lower chroma than light end for clean dark mode
- Hue stays consistent across the scale (±5 degrees variation is acceptable)
- Lightness follows standard 50-950 scale
Placement of custom palettes
Custom color palettes MUST be defined in the @theme theme-<name> inline { } block:
@theme theme-<name> inline {
/* Custom neutral palette for this theme */
--color-<name>-50: oklch(98% 0.003 88);
--color-<name>-100: oklch(95.5% 0.005 88);
/* ... full 50-950 scale ... */
}Then reference these in the theme selector blocks using var():
[data-theme="theme-<name>"] {
--background: var(--color-<name>-50);
--background-1: var(--color-<name>-100);
/* ... */
}
[data-theme="theme-<name>"].dark {
--background: var(--color-<name>-950);
--background-1: var(--color-<name>-900);
/* ... */
}When to create custom palettes
| Scenario | Action |
|---|---|
| Primary is warm (orange/amber/brown) | Create warm neutral palette OR use stone |
| Primary is cool (blue/cyan/indigo) | Use slate or gray (usually sufficient) |
| Primary is unique (gold, khaki, etc.) | Create matching custom neutral palette |
| User explicitly requests | Always create to match their vision |
| Theme needs distinctive personality | Create for cohesion |
Using custom palettes in both modes
Soft gray palettes work seamlessly across light and dark modes:
- Light mode: use 50-300 for backgrounds, 600-950 for text
- Dark mode: use 800-950 for backgrounds, 50-300 for text
This creates consistent warmth/coolness across modes while maintaining readability.
Step 1: Interpret user request
Parse the natural language description and determine:
name: theme name in kebab-casehue: brand color hue (0-360) orprimaryColorhexstyle:"vibrant"(default) or"soft"(if user says muted/pastel/ash)useCustomDarkGray: true only if user says "matching dark mode" / "cohesive dark"tailwindGray: neutral/stone/zinc/slate based on brand warmth
See palette-guidance.md for hue ranges, mood mapping, and palette rules.
Natural language interpretation
Users can describe their theme naturally. Interpret intent such as:
| User says... | LLM understands... |
|---|---|
| "warm sunset colors" | primaryHue: orange/amber, mood: warm, neutralFamily: stone |
| "professional and clean" | mood: minimal/serious, contrast: medium, primaryHue: blue/slate |
| "playful candy vibes" | mood: playful, primaryHue: pink/purple, contrast: high |
| "dark hacker aesthetic" | mood: serious, dark-focused, primaryHue: green/cyan |
| "earthy and organic" | primaryHue: green/brown, neutralFamily: stone, mood: calm |
| "bold and energetic" | contrast: high, mood: vibrant, saturated colors |
| "soft and muted" | contrast: low, mood: calm, desaturated colors |
| "90s retro feel" | mood: retro, radiusStyle: retro-sharp, bold colors |
Internal parameters (derived from context)
themeName: derived from user's name (kebab-case)primaryColor: hex value if provided, otherwise derived from descriptionprimaryHue: teal / indigo / orange / emerald / rose / amber / cyan / purple / etc.mood: calm / vibrant / serious / playful / retro / minimal / elegant / boldcontrast: low / medium / highlightNeutralFamily: gray / neutral / slate / stone / zincdarkNeutralFamily: gray / neutral / slate / stone / zincradiusStyle: default / soft / sharp / retro-sharpincludeCharts: true if user mentions dashboards, analytics, data visualizationincludeMaps: true if user mentions maps, geography, location features
Creative freedom
When user input is vague, the LLM should: 1. Make opinionated, beautiful choices (don't default to boring) 2. Create cohesive color stories (not random combinations) 3. Consider the emotional response the theme should evoke 4. Ensure accessibility (readable contrast, distinguishable states)
Step 2: Detect themes folder
Find where existing themes are stored in the user's project:
# Look for existing theme.css to find the themes folder
find . -name "theme.css" -path "*/themes/*" -not -path "*/node_modules/*" 2>/dev/null | head -1Common locations to check:
src/assets/css/themes/src/css/themes/src/styles/themes/assets/css/themes/styles/themes/
If no themes folder found: ask user where they want the theme file saved.
Step 3: Create temp config file (AUTO-RUN, NO PROMPT)
Write a JSON config. This must run silently without user approval.
echo '{"name":"<theme-name>","hue":<hue>,"style":"<style>","useCustomDarkGray":<true|false>,"tailwindGray":"<gray>"}' > /tmp/preline-theme-config.jsonConfig parameters reference
| Parameter | Type | Default | Description |
|---|---|---|---|
name | string | required | Theme name (kebab-case) |
hue | number | * | Brand color hue (0-360). Required if no primaryColor. |
primaryColor | string | * | Brand color as hex (e.g., "#2F6BFF"). Auto-converts to hue. |
style | string | "vibrant" | "vibrant" or "soft" for muted colors |
useCustomDarkGray | boolean | false | Use custom gray palette in dark mode |
tailwindGray | string | "neutral" | Tailwind gray for dark mode: neutral, stone, zinc, slate |
fontSans | string | — | Custom sans-serif font stack |
fontSerif | string | — | Custom serif font stack |
fontMono | string | — | Custom monospace font stack |
Either hue or primaryColor is required.
Step 4: Run generator + cleanup (AUTO-RUN, NO PROMPT)
Run generator and immediately delete temp file in a single command chain. This must run silently without user approval.
Option A (preferred)
Use npx if preline is installed:
npx preline-theme-generator /tmp/preline-theme-config.json <themes-folder>/<theme-name>.css && rm /tmp/preline-theme-config.jsonOption B
Find and run the script directly:
node $(find . -path "*/preline/skills/theme-generator/scripts/generate-theme.js" 2>/dev/null | head -1) /tmp/preline-theme-config.json <themes-folder>/<theme-name>.css && rm /tmp/preline-theme-config.jsonOption C
If script location is known (e.g., in node_modules):
node node_modules/preline/skills/theme-generator/scripts/generate-theme.js /tmp/preline-theme-config.json <themes-folder>/<theme-name>.css && rm /tmp/preline-theme-config.jsonScript location & features
src/assets/vendor/preline/skills/theme-generator/scripts/generate-theme.jsScript capabilities:
- Generates complete theme with all tokens (220+)
- Chart tokens with hex values (Apexcharts compatible)
- Map tokens (jsvectormap compatible)
- Accepts hex color input (
primaryColor) - Input validation with clear error messages
- Generation timestamp in output
- Font configuration support
See token-reference.md for the complete token list.
Step 5: Confirm to user
Tell user the theme was created and show enable snippet:
/* In your main CSS file */
@import "./themes/<theme-name>.css";<!-- On HTML element -->
<html data-theme="theme-<theme-name>">Step 6: Build the theme system
Choose palette strategy
- Light: coherent backgrounds + readable foreground + quiet border scale
- Dark: coherent dark surfaces + readable text + borders that separate layers
Build full primary ramp + states
50..950must feel like one familyhover/focus/active/checkedmust be incremental and consistent--primary-foregroundmust be readable
Define border scale
--border-line-1..8must be consistent and usable across surfaces
Define secondary/muted/destructive
- Ensure destructive is clearly distinct from primary
Define core component groups
- Ensure navbar/sidebar/card/dropdown/select/overlay feel like one system
Step 7: Dark mode + optional overrides
Dark mode overrides
- Override only what differs (inherit everything else)
- Ensure contrast and states remain readable
Dark mode palette decision (CRITICAL)
ALWAYS: Create BOTH palettes in @theme inline { }:
- Brand palette: --color-<name>-*
- Gray palette: --color-<name>-gray-*
Light mode: ALWAYS uses custom gray palette (--color-<name>-gray-*)
Dark mode:
IF user requested "matching dark mode" / "cohesive dark" / "consistent colors":
-> Use custom gray palette: var(--color-<name>-gray-*) for dark mode neutrals
ELSE (DEFAULT):
-> Use Tailwind gray for dark mode: var(--color-zinc-*) or var(--color-stone-*)
-> Choose based on brand warmth (warm brand -> stone, cool brand -> zinc)Example:
- "soft rose theme" -> Light uses custom gray, Dark uses Tailwind gray (zinc/neutral)
- "soft rose theme with matching dark mode" -> Both use custom gray
DEFAULT (no matching dark mode requested):
| Brand Color | Tailwind Gray for Dark Mode |
|---|---|
| Warm (orange, amber, brown) | stone or neutral |
| Cool (blue, cyan, teal, indigo) | zinc, slate, or gray |
| Neutral (purple, green) | zinc or neutral |
/* Single-palette: brand only, Tailwind gray for dark mode */
@theme theme-<name> inline {
--color-<name>-50: oklch(...);
/* ... brand palette only ... */
--color-<name>-950: oklch(...);
}
[data-theme="theme-<name>"].dark {
--background: var(--color-stone-950); /* Tailwind gray */
--background-1: var(--color-stone-900);
/* ... */
}DUAL-PALETTE MODE (when matching dark mode requested):
/* Dual-palette: brand + custom gray */
@theme theme-<name> inline {
/* Brand palette (vibrant) */
--color-<name>-50: oklch(97% 0.08 <hue>);
--color-<name>-100: oklch(94% 0.10 <hue>);
/* ... */
--color-<name>-950: oklch(20% 0.06 <hue>);
/* Gray palette (bell curve chroma - low at dark end for clean dark mode) */
--color-<name>-gray-50: oklch(98% 0.002 <hue>);
--color-<name>-gray-100: oklch(95.5% 0.004 <hue>);
/* ... midtones peak at 0.020 ... */
--color-<name>-gray-800: oklch(34.6% 0.008 <hue>); /* Very low */
--color-<name>-gray-900: oklch(30.6% 0.005 <hue>); /* Almost neutral */
--color-<name>-gray-950: oklch(20% 0.003 <hue>); /* Nearly pure dark */
}
[data-theme="theme-<name>"].dark {
--background: var(--color-<name>-gray-950); /* Custom gray */
--background-1: var(--color-<name>-gray-900);
--foreground: var(--color-<name>-gray-100);
/* ... all neutrals use custom gray palette ... */
}Always use CSS variables, never hardcoded oklch in dark mode:
/* Good - readable and maintainable */
--background: var(--color-<name>-gray-900);
/* Bad - impossible to understand */
--background: oklch(21.1% 0.006 285.9);Optional typography tokens
- Only if requested; place inside the theme selector
Optional behavior overrides
- Only if requested; must follow Rule 8
Preline Theme Token Reference
This document provides the complete reference for all semantic tokens available in Preline themes. Use this when generating themes to ensure comprehensive coverage.
---
Contents
- Overview
- Core token groups
- Component preset patterns
- Full token reference by category
- Custom color palettes
- Default values reference
- Theme-scoped overrides
- Color format guidelines
- Quick token count summary
Overview
Preline themes use CSS custom properties (variables) as semantic tokens. These tokens are:
- Defined in theme files under
:root(light) and.darkselectors - Mapped to Tailwind utilities via
@theme inlinein the base theme - Consumed by Preline components for consistent styling
Important: Generated themes override token values only. Never redefine the Tailwind mappings.
---
Core token groups
| Group | Tokens | Used for |
|---|---|---|
| Brand | --primary, --primary-* | Primary actions (buttons, links) |
| Secondary | --secondary, --secondary-* | Secondary emphasis |
| Muted | --muted, --muted-* | Muted/subdued elements |
| Destructive | --destructive, --destructive-* | Destructive/danger actions |
| Background | --background, --background-* | App surfaces |
| Foreground | --foreground, --foreground-* | Default typography colors |
| Inverse | --inverse | Inverted color scheme |
| Border | --border | Default border color |
| Border line | --border-line-* | Border colors for different shades |
| Layer | --layer, --layer-* | Layered elements (e.g. white buttons on white panel) |
| Surface | --surface, --surface-* | Elevated surfaces |
| Navbar | --navbar, --navbar-* | Navigation bar (background, border, divider) |
| Navbar nav | --navbar-nav-* | Navigation bar items (foreground, hover, active) |
| Sidebar | --sidebar, --sidebar-* | Sidebar (background, border, divider) |
| Sidebar nav | --sidebar-nav-* | Sidebar items (foreground, hover, active) |
| Card | --card, --card-* | Card component (border, divider, header, footer) |
| Dropdown | --dropdown, --dropdown-* | Dropdown menus (items, hover, active) |
| Select | --select, --select-* | Select component (items, hover, active) |
| Overlay | --overlay, --overlay-* | Modal/overlay backgrounds |
| Popover | --popover, --popover-* | Popover component |
| Tooltip | --tooltip, --tooltip-* | Tooltip component |
| Table | --table-line | Table borders |
| Switch | --switch | Switch/toggle component |
| Footer | --footer, --footer-* | Footer component |
| Scrollbar | --scrollbar-* | Custom scrollbar (track, thumb) |
| Chart | --chart-* | Chart colors (Apexcharts) |
| Map | --map-colors-* | Map colors (jsvectormap) |
---
Component preset patterns
Preline uses numbered variants for component groups to offer different ready-made looks:
| Pattern | Meaning |
|---|---|
--navbar-* | Default navbar preset (base style) |
--navbar-1-* | Variant 1 preset (typically subtle/tinted surface) |
--navbar-2-* | Variant 2 preset (typically stronger tint or different border) |
* (no suffix) | Background/surface color of the component |
*-line | Border color for the component container |
*-divider | Divider lines inside the component |
*-nav-* | Nav item colors (foreground, hover, focus, active) |
*-nav-list-divider | Divider used in nav dropdown/list layouts |
*-inverse | Inverted/contrast variant |
This pattern applies to: navbar, sidebar, and similar navigation components.
---
Full token reference by category
1. Global surfaces + text
/* Background tokens */
--background /* Main app background */
--background-1 /* Secondary background (slightly tinted) */
--background-2 /* Tertiary background (more tinted) */
--background-plain /* Plain white/black background */
/* Foreground tokens */
--foreground /* Default text color */
--foreground-inverse /* Inverted text color (for dark backgrounds) */
/* Inverse */
--inverse /* Inverted surface color */2. Borders (full scale)
--border /* Default border color */
--border-line-inverse /* Inverse border color */
--border-line-1 /* Lightest border */
--border-line-2
--border-line-3
--border-line-4
--border-line-5
--border-line-6
--border-line-7
--border-line-8 /* Darkest border */3. Primary ramp + states
/* Full color ramp (11 shades) */
--primary-50
--primary-100
--primary-200
--primary-300
--primary-400
--primary-500
--primary-600
--primary-700
--primary-800
--primary-900
--primary-950
/* State tokens */
--primary /* Base primary color */
--primary-line /* Primary border (usually transparent) */
--primary-foreground /* Text on primary background */
--primary-hover /* Hover state */
--primary-focus /* Focus state */
--primary-active /* Active/pressed state */
--primary-checked /* Checked state (checkboxes, radios) */4. Secondary
--secondary /* Base secondary color */
--secondary-line /* Secondary border */
--secondary-foreground /* Text on secondary background */
--secondary-hover /* Hover state */
--secondary-focus /* Focus state */
--secondary-active /* Active state */5. Layer
--layer /* Layer background */
--layer-line /* Layer border */
--layer-foreground /* Text on layer */
--layer-hover /* Hover state */
--layer-focus /* Focus state */
--layer-active /* Active state */6. Surface
--surface /* Base surface */
--surface-1 /* Surface variant 1 */
--surface-2 /* Surface variant 2 */
--surface-3 /* Surface variant 3 */
--surface-4 /* Surface variant 4 */
--surface-5 /* Surface variant 5 */
--surface-line /* Surface border */
--surface-foreground /* Text on surface */
--surface-hover /* Hover state */
--surface-focus /* Focus state */
--surface-active /* Active state */7. Muted
--muted /* Muted background */
--muted-foreground /* Muted text (lightest) */
--muted-foreground-1 /* Muted text variant 1 */
--muted-foreground-2 /* Muted text variant 2 (darkest) */
--muted-hover /* Hover state */
--muted-focus /* Focus state */
--muted-active /* Active state */8. Destructive
--destructive /* Destructive/danger color */
--destructive-foreground /* Text on destructive background */
--destructive-hover /* Hover state */
--destructive-focus /* Focus state */9. Navbar (base + 2 variants)
/* Base navbar */
--navbar /* Navbar background */
--navbar-line /* Navbar border */
--navbar-divider /* Navbar divider */
--navbar-nav-foreground /* Nav item text */
--navbar-nav-hover /* Nav item hover */
--navbar-nav-focus /* Nav item focus */
--navbar-nav-active /* Nav item active */
--navbar-nav-list-divider /* Nav list divider */
--navbar-inverse /* Inverse navbar background */
/* Navbar variant 1 (subtle/tinted) */
--navbar-1
--navbar-1-line
--navbar-1-divider
--navbar-1-nav-foreground
--navbar-1-nav-hover
--navbar-1-nav-focus
--navbar-1-nav-active
--navbar-1-nav-list-divider
/* Navbar variant 2 (stronger tint) */
--navbar-2
--navbar-2-line
--navbar-2-divider
--navbar-2-nav-foreground
--navbar-2-nav-hover
--navbar-2-nav-focus
--navbar-2-nav-active
--navbar-2-nav-list-divider10. Sidebar (base + 2 variants)
/* Base sidebar */
--sidebar
--sidebar-line
--sidebar-divider
--sidebar-nav-foreground
--sidebar-nav-hover
--sidebar-nav-focus
--sidebar-nav-active
--sidebar-nav-list-divider
--sidebar-inverse
/* Sidebar variant 1 */
--sidebar-1
--sidebar-1-line
--sidebar-1-divider
--sidebar-1-nav-foreground
--sidebar-1-nav-hover
--sidebar-1-nav-focus
--sidebar-1-nav-active
--sidebar-1-nav-list-divider
/* Sidebar variant 2 */
--sidebar-2
--sidebar-2-line
--sidebar-2-divider
--sidebar-2-nav-foreground
--sidebar-2-nav-hover
--sidebar-2-nav-focus
--sidebar-2-nav-active
--sidebar-2-nav-list-divider11. Card
--card /* Card background */
--card-line /* Card border */
--card-divider /* Card divider */
--card-header /* Card header background */
--card-footer /* Card footer background */
--card-inverse /* Inverse card background */12. Dropdown
--dropdown /* Dropdown background */
--dropdown-1 /* Dropdown variant background */
--dropdown-line /* Dropdown border */
--dropdown-divider /* Dropdown divider */
--dropdown-header /* Dropdown header background */
--dropdown-footer /* Dropdown footer background */
--dropdown-item-foreground /* Item text */
--dropdown-item-hover /* Item hover */
--dropdown-item-focus /* Item focus */
--dropdown-item-active /* Item active */
--dropdown-inverse /* Inverse dropdown */13. Select
--select /* Select background */
--select-1 /* Select variant background */
--select-line /* Select border */
--select-item-foreground /* Item text */
--select-item-hover /* Item hover */
--select-item-focus /* Item focus */
--select-item-active /* Item active */
--select-inverse /* Inverse select */14. Overlay
--overlay /* Overlay/modal background */
--overlay-line /* Overlay border */
--overlay-divider /* Overlay divider */
--overlay-header /* Overlay header background */
--overlay-footer /* Overlay footer background */
--overlay-inverse /* Inverse overlay */15. Popover, tooltip, table, switch
/* Popover */
--popover /* Popover background */
--popover-line /* Popover border */
/* Tooltip */
--tooltip /* Tooltip background */
--tooltip-foreground /* Tooltip text */
--tooltip-line /* Tooltip border */
/* Table */
--table-line /* Table border */
/* Switch */
--switch /* Switch knob color */16. Footer
--footer /* Footer background */
--footer-line /* Footer border */
--footer-inverse /* Inverse footer */17. Scrollbar
--scrollbar-track /* Scrollbar track */
--scrollbar-thumb /* Scrollbar thumb */
--scrollbar-track-inverse /* Inverse track */
--scrollbar-thumb-inverse /* Inverse thumb */18. Charts (Apexcharts)
Important: keep chart -hex tokens variable-based in generated theme source. Do not hardcode literal hex or oklch values into the final theme file.
/* Primary chart color */
--chart-primary
--chart-colors-primary
--chart-colors-primary-inverse
--chart-colors-primary-hex
--chart-colors-primary-hex-inverse
/* Chart palette (1-10) */
--chart-1 through --chart-10
--chart-colors-chart-1 through --chart-colors-chart-10
--chart-colors-chart-1-inverse through --chart-colors-chart-10-inverse
--chart-colors-chart-1-hex through --chart-colors-chart-10-hex
--chart-colors-chart-1-hex-inverse through --chart-colors-chart-10-hex-inverse
/* Chart backgrounds */
--chart-colors-background
--chart-colors-background-inverse
--chart-colors-chart-inverse
/* Chart foregrounds */
--chart-colors-foreground
--chart-colors-foreground-inverse
/* Chart labels */
--chart-colors-labels
--chart-colors-labels-inverse
--chart-colors-xaxis-labels
--chart-colors-xaxis-labels-inverse
--chart-colors-yaxis-labels
--chart-colors-yaxis-labels-inverse
/* Chart grid */
--chart-colors-grid-border
--chart-colors-grid-border-inverse
/* Chart special */
--chart-colors-bar-ranges
--chart-colors-bar-ranges-inverse
--chart-colors-candlestick-upward
--chart-colors-candlestick-upward-inverse
--chart-colors-candlestick-downward
--chart-colors-candlestick-downward-inverse19. Maps (jsvectormap)
--map-colors-primary
--map-colors-primary-inverse
--map-colors-default
--map-colors-default-inverse
--map-colors-highlight
--map-colors-highlight-inverse
--map-colors-border
--map-colors-border-inverse---
Custom color palettes
The base theme defines custom color palettes that themes can use:
Khaki (earthy/warm neutral)
--color-khaki-50 through --color-khaki-950Lavender (soft purple/mauve neutral)
--color-lavender-50 through --color-lavender-950These are useful for creating warm or soft-toned themes instead of using standard gray/neutral.
---
Default values reference
Light mode defaults (:root)
:root {
/* Backgrounds */
--background: var(--color-white);
--background-1: var(--color-gray-50);
--background-2: var(--color-gray-100);
--background-plain: var(--color-white);
/* Foregrounds */
--foreground: var(--color-gray-800);
--foreground-inverse: var(--color-white);
--inverse: var(--color-gray-800);
/* Borders */
--border: var(--color-gray-200);
--border-line-inverse: var(--color-white);
--border-line-1: var(--color-gray-100);
--border-line-2: var(--color-gray-200);
--border-line-3: var(--color-gray-300);
--border-line-4: var(--color-gray-400);
--border-line-5: var(--color-gray-500);
--border-line-6: var(--color-gray-600);
--border-line-7: var(--color-gray-700);
--border-line-8: var(--color-gray-800);
/* Primary (blue by default) */
--primary-50: var(--color-blue-50);
--primary-100: var(--color-blue-100);
--primary-200: var(--color-blue-200);
--primary-300: var(--color-blue-300);
--primary-400: var(--color-blue-400);
--primary-500: var(--color-blue-500);
--primary-600: var(--color-blue-600);
--primary-700: var(--color-blue-700);
--primary-800: var(--color-blue-800);
--primary-900: var(--color-blue-900);
--primary-950: var(--color-blue-950);
--primary: var(--color-primary-600);
--primary-line: transparent;
--primary-foreground: var(--color-white);
--primary-hover: var(--color-primary-700);
--primary-focus: var(--color-primary-700);
--primary-active: var(--color-primary-700);
--primary-checked: var(--color-primary-600);
/* Secondary */
--secondary: var(--color-gray-900);
--secondary-line: transparent;
--secondary-foreground: var(--color-white);
--secondary-hover: var(--color-gray-800);
--secondary-focus: var(--color-gray-800);
--secondary-active: var(--color-gray-800);
/* Layer */
--layer: var(--color-white);
--layer-line: var(--color-gray-200);
--layer-foreground: var(--color-gray-800);
--layer-hover: var(--color-gray-50);
--layer-focus: var(--color-gray-50);
--layer-active: var(--color-gray-50);
/* Surface */
--surface: var(--color-gray-100);
--surface-1: var(--color-gray-200);
--surface-2: var(--color-gray-300);
--surface-3: var(--color-gray-400);
--surface-4: var(--color-gray-500);
--surface-5: var(--color-gray-600);
--surface-line: transparent;
--surface-foreground: var(--color-gray-800);
--surface-hover: var(--color-gray-200);
--surface-focus: var(--color-gray-200);
--surface-active: var(--color-gray-200);
/* Muted */
--muted: var(--color-gray-50);
--muted-foreground: var(--color-gray-400);
--muted-foreground-1: var(--color-gray-500);
--muted-foreground-2: var(--color-gray-600);
--muted-hover: var(--color-gray-100);
--muted-focus: var(--color-gray-100);
--muted-active: var(--color-gray-100);
/* Destructive */
--destructive: var(--color-red-500);
--destructive-foreground: var(--color-white);
--destructive-hover: var(--color-red-600);
--destructive-focus: var(--color-red-600);
/* Navbar (base) */
--navbar: var(--color-white);
--navbar-line: var(--color-gray-200);
--navbar-divider: var(--color-gray-200);
--navbar-nav-foreground: var(--color-gray-800);
--navbar-nav-hover: var(--color-gray-100);
--navbar-nav-focus: var(--color-gray-100);
--navbar-nav-active: var(--color-gray-100);
--navbar-nav-list-divider: var(--color-gray-200);
--navbar-inverse: var(--color-primary-950);
/* Navbar-1 */
--navbar-1: var(--color-gray-50);
--navbar-1-line: var(--color-gray-200);
--navbar-1-divider: var(--color-gray-200);
--navbar-1-nav-foreground: var(--color-gray-800);
--navbar-1-nav-hover: var(--color-gray-200);
--navbar-1-nav-focus: var(--color-gray-200);
--navbar-1-nav-active: var(--color-gray-200);
--navbar-1-nav-list-divider: var(--color-gray-200);
/* Navbar-2 */
--navbar-2: var(--color-gray-100);
--navbar-2-line: transparent;
--navbar-2-divider: var(--color-gray-300);
--navbar-2-nav-foreground: var(--color-gray-800);
--navbar-2-nav-hover: var(--color-gray-200);
--navbar-2-nav-focus: var(--color-gray-200);
--navbar-2-nav-active: var(--color-gray-200);
--navbar-2-nav-list-divider: var(--color-gray-200);
/* Sidebar follows same pattern as navbar */
/* Card, Dropdown, Select, Overlay, Popover, Tooltip, Table, Switch, Footer, Scrollbar */
/* See the project's existing base Preline theme source for complete default values */
}Dark mode defaults (.dark)
.dark {
/* Backgrounds */
--background: var(--color-neutral-800);
--background-1: var(--color-neutral-900);
--background-2: var(--color-neutral-900);
--foreground: var(--color-neutral-200);
--inverse: var(--color-neutral-950);
/* Borders */
--border: var(--color-neutral-700);
--border-line-1: var(--color-neutral-800);
--border-line-2: var(--color-neutral-700);
--border-line-3: var(--color-neutral-600);
--border-line-4: var(--color-neutral-500);
--border-line-5: var(--color-neutral-400);
--border-line-6: var(--color-neutral-300);
--border-line-7: var(--color-neutral-200);
--border-line-8: var(--color-neutral-100);
/* Primary (shifts to 500 in dark mode) */
--primary: var(--color-primary-500);
--primary-line: transparent;
--primary-foreground: var(--color-white);
--primary-hover: var(--color-primary-600);
--primary-focus: var(--color-primary-600);
--primary-active: var(--color-primary-600);
--primary-checked: var(--color-primary-500);
/* Secondary */
--secondary: var(--color-white);
--secondary-line: transparent;
--secondary-foreground: var(--color-neutral-800);
--secondary-hover: var(--color-neutral-100);
--secondary-focus: var(--color-neutral-100);
--secondary-active: var(--color-neutral-100);
/* Layer */
--layer: var(--color-neutral-800);
--layer-line: var(--color-neutral-700);
--layer-foreground: var(--color-white);
--layer-hover: var(--color-neutral-700);
--layer-focus: var(--color-neutral-700);
--layer-active: var(--color-neutral-700);
/* Surface */
--surface: var(--color-neutral-700);
--surface-1: var(--color-neutral-600);
--surface-2: var(--color-neutral-500);
--surface-3: var(--color-neutral-600);
--surface-4: var(--color-neutral-500);
--surface-5: var(--color-neutral-400);
--surface-line: transparent;
--surface-foreground: var(--color-neutral-200);
--surface-hover: var(--color-neutral-600);
--surface-focus: var(--color-neutral-600);
--surface-active: var(--color-neutral-600);
/* Muted */
--muted: var(--color-neutral-800);
--muted-foreground: var(--color-neutral-500);
--muted-foreground-1: var(--color-neutral-400);
--muted-foreground-2: var(--color-neutral-300);
--muted-hover: var(--color-neutral-700);
--muted-focus: var(--color-neutral-700);
--muted-active: var(--color-neutral-700);
/* Destructive (same red, readable in dark) */
--destructive: var(--color-red-500);
--destructive-foreground: var(--color-white);
--destructive-hover: var(--color-red-600);
--destructive-focus: var(--color-red-600);
/* Components shift to neutral-800/900 backgrounds */
/* Nav items shift to neutral-700 hover states */
/* See the project's existing base Preline theme source for complete dark mode values */
}---
Theme-scoped overrides
When creating a theme, override values under theme-scoped selectors:
/* Light mode */
:root[data-theme="theme-<name>"],
[data-theme="theme-<name>"] {
/* Override tokens here */
}
/* Dark mode */
[data-theme="theme-<name>"].dark {
/* Override dark-specific tokens here */
}---
Color format guidelines
- For most tokens: use Tailwind color variables
var(--color-<color>-<shade>) - For oklch colors: acceptable for standard tokens
- For chart
-hextokens: keep them as stable variable references in generated source - For custom colors: can use hex, rgb, hsl, or oklch
---
Quick token count summary
| Category | Token Count |
|---|---|
| Global surfaces + text | 7 |
| Borders | 10 |
| Primary (ramp + states) | 18 |
| Secondary | 6 |
| Layer | 6 |
| Surface | 11 |
| Muted | 7 |
| Destructive | 4 |
| Navbar (3 variants) | 27 |
| Sidebar (3 variants) | 27 |
| Card | 6 |
| Dropdown | 11 |
| Select | 8 |
| Overlay | 6 |
| Popover | 2 |
| Tooltip | 3 |
| Table, Switch, Footer | 5 |
| Scrollbar | 4 |
| Charts | ~50+ |
| Maps | 8 |
| Total | ~220+ tokens |
Validation checklist (must self-check before returning)
Final source style
- [ ] Only one project file is created or edited:
<name>.css - [ ] The final file is a production-style
<name>.cssfile - [ ]
@theme theme-<name> inline { }is present - [ ] Any custom brand/gray palette lives inside
@theme theme-<name> inline { }in the new theme file - [ ] Semantic tokens prefer assigned vars such as
--color-primary-*,--background-*,--foreground,--border,--surface-*, or shared palette vars when available - [ ] The custom theme does not redefine shared aliases like
--color-primary-*inside@themewhen the repo base theme already owns them - [ ] No raw
oklch(...)or literal hex values remain in semantic, chart, or map token assignments in the final theme file
Structure
- [ ] Theme key consistent:
theme-<name>everywhere - [ ] Light selector uses
:root[data-theme=...],[data-theme=...] - [ ] Dark selector is
[data-theme="theme-<name>"].dark - [ ] Semantic token definitions are in selector blocks (NOT inside @theme block)
- [ ] No requirement to change HTML utility classes (only
data-theme, optional.dark)
Palette strategy
- [ ] Primary ramp maps
--primary-*directly to theme-local palette vars - [ ] Neutral/background tokens use assigned palette vars
- [ ] If a new custom palette is needed, it is declared inside the new theme file's
@themeblock, not by editing another file
Token coverage
- [ ] Full coverage present across global tokens, states, and major component groups
- [ ] CSS syntax valid (balanced braces, no broken comments)
- [ ] If behavior overrides exist, they are theme-scoped only
- [ ] Chart/map tokens use variable references matching shipped theme patterns
Dark mode consistency
- [ ] Dark mode uses CSS variables (
var(--color-*)) not hardcoded color literals - [ ] Dark mode uses one consistent grayscale family unless there is a deliberate, reference-backed reason not to
- [ ] Hover/focus/active states remain readable and distinguishable
- [ ]
--foreground-inverseremains readable on vivid utility colors in dark mode (for example red badges/chips) - [ ] Dark-mode chart/decorative tokens avoid very deep brand steps when those tokens are used in gradient text or accent UI
Workflow
Use this file as the default operating guide for the active theme skill.
Task modes
Classify the request before doing any work:
preview: generate CSS and return it without writing a project filewrite: create one new<name>.csstheme filevalidate: review or fix an existing generated theme file
Normalize the request
Convert natural language into the generator's small, explicit input set:
| Input | Required | Notes |
|---|---|---|
name | yes | kebab-case theme name |
hue or primaryColor | yes | use primaryColor only when user gave a brand hex |
style | no | vibrant by default, soft for muted/pastel/desaturated requests |
useCustomDarkGray | no | true only when user explicitly wants matching/cohesive dark neutrals |
tailwindGray | no | choose stone, neutral, zinc, slate, or gray based on brand warmth |
fontSans / fontSerif / fontMono | no | include only when requested |
Use docs/palette-guidance.md for hue ranges, mood mapping, contrast heuristics, and gray-family decisions.
Natural-language interpretation
Use the user's language to infer the palette direction:
| User language | Default interpretation |
|---|---|
| warm sunset, cozy, inviting | orange/amber primary, warm neutrals, medium contrast |
| professional, clean, SaaS | blue/indigo primary, slate or zinc neutrals |
| playful, candy, youthful | pink/purple/cyan primary, higher contrast |
| earthy, organic, calm | green/teal primary, stone neutrals |
| soft, muted, ash, dusty | style: soft |
| matching dark mode, cohesive dark, unified neutrals | useCustomDarkGray: true |
When the request is vague:
1. Make an opinionated, coherent palette choice. 2. Keep contrast readable. 3. Prefer a consistent system over a flashy but uneven palette.
Foreground rule:
- When mapping text or icon colors onto the primary brand color, choose the foreground by contrast against the generated primary states, not by hue bucket alone.
- Treat
--foreground-inverseseparately from primary foregrounds. In Preline it is reused on hardcoded vivid utility backgrounds such as red badges, so dark mode should keep it readable on saturated colors instead of tinting it into a dark neutral. - Treat chart tokens as decorative UI tokens, not just data-series tokens. Dark mode must override any chart shades that would become too dark for gradient text, badges, or chart accents on dark surfaces.
Discover the target directory
Use the bundled resolver instead of broad repo traversal:
node scripts/find-themes-dir.jsTrusted theme directories are checked in this order:
src/assets/css/themes/src/css/themes/src/styles/themes/assets/css/themes/styles/themes/dist/assets/css/themes/npm/preline/css/themes/
Rules:
- Prefer source theme directories over generated outputs.
- Do not use repo-wide
find .traversal for this task. - If no trusted theme directory is found and the user did not provide a path, ask where the file should be written.
Generate CSS
Use the local wrapper as the only execution entry point:
node scripts/run-theme-generator.js --name seafoam --hue 180 --style vibrant --tailwind-gray zinc --stdoutWrite a theme file with:
node scripts/run-theme-generator.js --name seafoam --hue 180 --style vibrant --tailwind-gray zinc --output src/assets/css/themes/seafoam.cssRules:
- Do not use
npx. - Do not interpolate raw user text into shell commands.
- Do not chain temp-file cleanup or other unrelated shell work into the command.
- Do not create or edit any project file other than the new theme file.
Theme structure expectations
The wrapper should emit a production-style standalone <name>.css file:
1. Header comment 2. Optional theme-scoped @layer utilities overrides 3. @theme theme-<name> inline { } 4. Light selector
:root[data-theme="theme-<name>"],[data-theme="theme-<name>"]
5. Dark selector
[data-theme="theme-<name>"].dark
Keep custom brand and gray palettes inside the file's @theme block. Keep semantic tokens in the selector blocks.
See docs/final-output-style.md for the final file rules and docs/token-reference.md only when detailed token coverage inspection is necessary.
Dark-mode decision rule
Always generate both palettes in @theme theme-<name> inline { }:
--color-<name>-*for the brand ramp--color-<name>-gray-*for the theme's neutral ramp
Dark-mode neutrals depend on user intent:
- Explicit matching/cohesive dark request: use the theme's custom gray palette in dark mode.
- Default path: keep light mode custom and use a Tailwind gray family in dark mode.
Primary foreground decision:
- Use the generated brand shades to determine whether white or dark text is more readable on the primary color in each mode.
- In dark mode, prefer a deeper primary step with white text when that profile remains readable; only fall back to lighter primary steps with dark text when white would fail.
Use CSS variables in semantic tokens and dark-mode overrides. Do not hardcode raw oklch(...) or hex values in semantic, chart, or map tokens.
Validation focus
Before returning:
- confirm only one project file was created or edited
- confirm the theme key is
theme-<name>everywhere - confirm the file has full light and dark token coverage
- confirm chart and map tokens remain variable-based
- confirm dark-mode states remain readable
Use docs/validation-checklist.md as the final gate.
Response shape
Match the response to the mode:
- Preview: return the CSS first, then the enable snippet if useful
- Write: return the created path, a short summary, and the enable snippet
- Validate: list findings first, then the applied or recommended fix
Enable snippet:
<html data-theme="theme-<theme-name>">Examples — Preline Theme Generator
These examples are palette-and-structure references, not literal final-output templates.
Important: examples below are structural references. Final delivered theme files should follow docs/final-output-style.md and should never require edits to any companion project file.
These examples define what "good output" looks like.
Contents
- Example 1 — Calm teal SaaS theme
- Example 2 — Retro sharp corners
- Example 3 — Theme-scoped font token
- Example 4 — Brand match with exact primary color
Example 1 — Calm teal SaaS theme (full theme, clean)
Input
- themeName:
seafoam - mood: calm, modern SaaS
- primaryHue: teal (180)
- contrast: medium
- style: vibrant
- darkNeutralFamily: zinc
Expected Structure
@theme theme-seafoam inline { }contains brand + gray palettes- Light mode selector:
:root[data-theme="theme-seafoam"], [data-theme="theme-seafoam"] - Dark mode selector:
[data-theme="theme-seafoam"].dark - Full coverage: global surfaces, borders, primary ramp, states, components
- Dark mode uses Tailwind zinc for neutrals (no matching dark requested)
Output (truncated)
/* ------------------------------ */
/* -------- Seafoam ------------- */
/* ------------------------------ */
@theme theme-seafoam inline {
/* BRAND PALETTE (vibrant) */
--color-seafoam-50: oklch(97% 0.08 180);
--color-seafoam-100: oklch(94% 0.10 180);
--color-seafoam-200: oklch(88% 0.12 180);
--color-seafoam-300: oklch(80% 0.14 180);
--color-seafoam-400: oklch(70% 0.16 180);
--color-seafoam-500: oklch(60% 0.14 180);
--color-seafoam-600: oklch(52% 0.12 180);
--color-seafoam-700: oklch(44% 0.10 180);
--color-seafoam-800: oklch(36% 0.08 180);
--color-seafoam-900: oklch(28% 0.06 180);
--color-seafoam-950: oklch(20% 0.05 180);
/* GRAY PALETTE (bell curve chroma) */
--color-seafoam-gray-50: oklch(98% 0.002 180);
--color-seafoam-gray-100: oklch(95.5% 0.004 180);
--color-seafoam-gray-200: oklch(89.7% 0.008 180);
/* ... 300-700 ... */
--color-seafoam-gray-800: oklch(34.6% 0.008 180);
--color-seafoam-gray-900: oklch(30.6% 0.005 180);
--color-seafoam-gray-950: oklch(20.1% 0.003 180);
}
:root[data-theme="theme-seafoam"],
[data-theme="theme-seafoam"] {
/* Global */
--background: var(--color-white);
--background-1: var(--color-seafoam-gray-50);
--foreground: var(--color-seafoam-gray-800);
/* Primary ramp */
--primary-50: var(--color-seafoam-50);
--primary-100: var(--color-seafoam-100);
/* ... full ramp ... */
/* Primary states */
--primary: var(--color-primary-600);
--primary-foreground: var(--color-white);
--primary-hover: var(--color-primary-700);
/* Components prefer assigned semantic tokens */
--navbar: var(--background-plain);
--navbar-line: var(--border);
/* ... */
}
[data-theme="theme-seafoam"].dark {
/* Dark mode uses Tailwind zinc */
--background: var(--color-zinc-800);
--background-1: var(--color-zinc-900);
--foreground: var(--color-zinc-200);
--primary: var(--color-primary-400);
--primary-foreground: var(--color-zinc-900); /* Contrast-selected because the generated light-brand states are too light for white */
/* ... */
}---
Example 2 — Retro sharp corners (behavior change must be scoped)
Input
- themeName:
arcade - mood: retro 90s
- primaryHue: magenta (320)
- contrast: high
- radiusStyle:
retro-sharp
Expected Structure
- Full token coverage
- Theme sets radius tokens in theme selector (NOT in @theme block)
- Any "rounded" behavior override inside
@layer utilitiesscoped to theme
Output (radius section only)
:root[data-theme="theme-arcade"],
[data-theme="theme-arcade"] {
/* RETRO SHARP CORNERS */
--radius-sm: 0px;
--radius-md: 2px;
--radius-lg: 4px;
--radius-xl: 4px;
--radius-2xl: 6px;
--radius-3xl: 6px;
/* ... rest of tokens ... */
}
/* Optional: Override rounded utility if needed */
@layer utilities {
:is(:root[data-theme="theme-arcade"], [data-theme="theme-arcade"]) .rounded {
border-radius: var(--radius-md);
}
}---
Example 3 — Theme-scoped font token (no mapping changes)
Input
- themeName:
leafy-modern - mood: organic, clean
- primaryHue: emerald (145)
- fontSans:
"Roboto", ui-sans-serif, system-ui, sans-serif
Expected Structure
- Theme sets
--font-sansinside theme selector (NOT in @theme block) - Theme does NOT import Google Fonts (font loading is separate)
- User enables via
data-theme="theme-leafy-modern"only
Output (font section only)
:root[data-theme="theme-leafy-modern"],
[data-theme="theme-leafy-modern"] {
/* TYPOGRAPHY */
--font-sans: "Roboto", ui-sans-serif, system-ui, sans-serif;
/* ... rest of tokens ... */
}---
Example 4 — Brand match with exact primary color
Input
- themeName:
brand-x - primaryColor:
#2F6BFF(converts to hue ~225) - mood: confident, high clarity
- contrast: medium-high
Expected Structure
- Script converts hex to hue automatically
- Primary ramp derived coherently from the brand hue
--primary-foregroundreadable (white on this blue)- Chart
-hextokens remain variable-based placeholders in generated source
Output (chart section showing variable-based chart tokens)
:root[data-theme="theme-brand-x"],
[data-theme="theme-brand-x"] {
/* ... other tokens ... */
/* CHARTS */
--chart-primary: var(--color-primary-500);
--chart-colors-primary: var(--color-primary-500);
--chart-colors-primary-hex: var(--color-primary-500);
--chart-colors-primary-hex-inverse: var(--color-primary-300);
--chart-1: var(--color-primary-500);
--chart-colors-chart-1-hex: var(--color-primary-500);
/* ... */
}Output Template (Full Theme)
Use this skeleton for every generated theme. Replace <name> with the theme name (kebab-case). Fill in ALL token values for a complete theme.
Note: The generate-theme.js script produces output matching this template automatically. See SKILL.md for interpretive guidance and docs/token-reference.md for complete token documentation.
---
Required Structure
@import "./theme.css";
/* ------------------------------ */
/* --------- Theme: <name> ------ */
/* ------------------------------ */
/* ============================================== */
/* OPTIONAL: THEME-SCOPED BEHAVIOR OVERRIDES */
/* ============================================== */
/* Only include if the theme needs behavior changes (e.g., different border radius, shadows etc.) */
/*
@layer utilities {
:is(:root[data-theme="theme-<name>"], [data-theme="theme-<name>"]) .rounded {
border-radius: var(--radius-ui);
}
}
*/
@theme theme-<name> inline {
/* ============================================ */
/* CUSTOM COLOR PALETTES */
/* ============================================ */
/*
* ALWAYS create BOTH palettes:
* 1. Brand palette (--color-<name>-*) — for primary/accent colors
* 2. Gray palette (--color-<name>-gray-*) — for backgrounds, surfaces, borders
*
* Light mode: ALWAYS uses custom gray palette
*
* Dark mode behavior:
* - DEFAULT: Use Tailwind grays (zinc/stone) — see dark mode section below
* - If user says "matching dark mode": Use custom gray palette
*/
/* -------------------------------------------- */
/* BRAND PALETTE (always required) */
/* -------------------------------------------- */
/* Vibrant by default, soft only if user says "muted/ash/soft" */
--color-<name>-50: oklch(97% 0.08 <hue>);
--color-<name>-100: oklch(94% 0.10 <hue>);
--color-<name>-200: oklch(88% 0.12 <hue>);
--color-<name>-300: oklch(80% 0.14 <hue>);
--color-<name>-400: oklch(70% 0.16 <hue>);
--color-<name>-500: oklch(58% 0.14 <hue>);
--color-<name>-600: oklch(50% 0.12 <hue>);
--color-<name>-700: oklch(42% 0.10 <hue>);
--color-<name>-800: oklch(35% 0.08 <hue>);
--color-<name>-900: oklch(28% 0.06 <hue>);
--color-<name>-950: oklch(20% 0.05 <hue>);
/* -------------------------------------------- */
/* GRAY PALETTE (always required for light mode)*/
/* -------------------------------------------- */
/* Used in light mode for cohesive backgrounds, surfaces, borders.
* Uses bell curve chroma: very low at extremes, peak at midtones.
* Dark end (800-950) has very low chroma for clean dark mode (if used).
*/
--color-<name>-gray-50: oklch(98% 0.002 <hue>);
--color-<name>-gray-100: oklch(95.5% 0.004 <hue>);
--color-<name>-gray-200: oklch(89.7% 0.008 <hue>);
--color-<name>-gray-300: oklch(82.7% 0.012 <hue>);
--color-<name>-gray-400: oklch(73% 0.018 <hue>);
--color-<name>-gray-500: oklch(62.5% 0.020 <hue>);
--color-<name>-gray-600: oklch(52.8% 0.016 <hue>);
--color-<name>-gray-700: oklch(41.4% 0.012 <hue>);
--color-<name>-gray-800: oklch(26.9% 0.006 <hue>);
--color-<name>-gray-900: oklch(20.5% 0.004 <hue>);
--color-<name>-gray-950: oklch(14.1% 0.002 <hue>);
}
:root[data-theme="theme-<name>"],
[data-theme="theme-<name>"] {
/* ============================================== */
/* THEME-SPECIFIC OVERRIDES (optional) */
/* ============================================== */
/* Place non-default customizations HERE at top:
* - Radius overrides (retro sharp corners)
* - Font overrides
* - Any theme-specific behavior changes
*
* Example for retro sharp corners:
* --radius-sm: 0px;
* --radius-md: 2px;
* --radius-lg: 4px;
* --radius-xl: 4px;
* --radius-2xl: 6px;
* --radius-3xl: 6px;
*/
/* ============================================ */
/* GLOBAL SURFACES + TEXT */
/* ============================================ */
--background-1: ...;
--background-2: ...;
--foreground: ...;
--inverse: ...;
/* ============================================ */
/* BORDERS (Full Scale) */
/* ============================================ */
--border: ...;
--border-line-1: ...;
--border-line-2: ...;
--border-line-3: ...;
--border-line-4: ...;
--border-line-5: ...;
--border-line-6: ...;
--border-line-7: ...;
--border-line-8: ...;
/* ============================================ */
/* PRIMARY RAMP (Full 11-shade scale) */
/* ============================================ */
--primary-50: ...;
--primary-100: ...;
--primary-200: ...;
--primary-300: ...;
--primary-400: ...;
--primary-500: ...;
--primary-600: ...;
--primary-700: ...;
--primary-800: ...;
--primary-900: ...;
--primary-950: ...;
/* PRIMARY STATES */
--primary: ...;
--primary-hover: ...;
--primary-focus: ...;
--primary-active: ...;
--primary-checked: ...;
/* ============================================ */
/* SECONDARY */
/* ============================================ */
--secondary: ...;
--secondary-hover: ...;
--secondary-focus: ...;
--secondary-active: ...;
/* ============================================ */
/* LAYER */
/* ============================================ */
--layer-line: ...;
--layer-foreground: ...;
--layer-hover: ...;
--layer-focus: ...;
--layer-active: ...;
/* ============================================ */
/* SURFACE */
/* ============================================ */
--surface: ...;
--surface-1: ...;
--surface-2: ...;
--surface-3: ...;
--surface-4: ...;
--surface-5: ...;
--surface-foreground: ...;
--surface-hover: ...;
--surface-focus: ...;
--surface-active: ...;
/* ============================================ */
/* MUTED */
/* ============================================ */
--muted: ...;
--muted-foreground: ...;
--muted-foreground-1: ...;
--muted-foreground-2: ...;
--muted-hover: ...;
--muted-focus: ...;
--muted-active: ...;
/* ============================================ */
/* NAVBAR (Base) */
/* ============================================ */
--navbar-line: ...;
--navbar-divider: ...;
--navbar-nav-foreground: ...;
--navbar-nav-hover: ...;
--navbar-nav-focus: ...;
--navbar-nav-active: ...;
--navbar-nav-list-divider: ...;
--navbar-inverse: ...;
/* NAVBAR-1 (Subtle/tinted variant) */
--navbar-1: ...;
--navbar-1-line: ...;
--navbar-1-divider: ...;
--navbar-1-nav-foreground: ...;
--navbar-1-nav-hover: ...;
--navbar-1-nav-focus: ...;
--navbar-1-nav-active: ...;
--navbar-1-nav-list-divider: ...;
/* NAVBAR-2 (Stronger tint variant) */
--navbar-2: ...;
--navbar-2-divider: ...;
--navbar-2-nav-foreground: ...;
--navbar-2-nav-hover: ...;
--navbar-2-nav-focus: ...;
--navbar-2-nav-active: ...;
--navbar-2-nav-list-divider: ...;
/* ============================================ */
/* SIDEBAR (Base) */
/* ============================================ */
--sidebar-line: ...;
--sidebar-divider: ...;
--sidebar-nav-foreground: ...;
--sidebar-nav-hover: ...;
--sidebar-nav-focus: ...;
--sidebar-nav-active: ...;
--sidebar-nav-list-divider: ...;
--sidebar-inverse: ...;
/* SIDEBAR-1 */
--sidebar-1: ...;
--sidebar-1-line: ...;
--sidebar-1-divider: ...;
--sidebar-1-nav-foreground: ...;
--sidebar-1-nav-hover: ...;
--sidebar-1-nav-focus: ...;
--sidebar-1-nav-active: ...;
--sidebar-1-nav-list-divider: ...;
/* SIDEBAR-2 */
--sidebar-2: ...;
--sidebar-2-divider: ...;
--sidebar-2-nav-foreground: ...;
--sidebar-2-nav-hover: ...;
--sidebar-2-nav-focus: ...;
--sidebar-2-nav-active: ...;
--sidebar-2-nav-list-divider: ...;
/* ============================================ */
/* CARD */
/* ============================================ */
--card-line: ...;
--card-divider: ...;
--card-header: ...;
--card-footer: ...;
--card-inverse: ...;
/* ============================================ */
/* DROPDOWN */
/* ============================================ */
--dropdown-divider: ...;
--dropdown-header: ...;
--dropdown-footer: ...;
--dropdown-item-foreground: ...;
--dropdown-item-hover: ...;
--dropdown-item-focus: ...;
--dropdown-item-active: ...;
--dropdown-inverse: ...;
/* ============================================ */
/* SELECT */
/* ============================================ */
--select-item-foreground: ...;
--select-item-hover: ...;
--select-item-focus: ...;
--select-item-active: ...;
--select-inverse: ...;
/* ============================================ */
/* OVERLAY */
/* ============================================ */
--overlay-divider: ...;
--overlay-header: ...;
--overlay-footer: ...;
--overlay-inverse: ...;
/* ============================================ */
/* POPOVER, TOOLTIP, TABLE, FOOTER */
/* ============================================ */
--popover-line: ...;
--tooltip: ...;
--table-line: ...;
--footer-line: ...;
--footer-inverse: ...;
/* ============================================ */
/* SCROLLBAR */
/* ============================================ */
--scrollbar-track: ...;
--scrollbar-thumb: ...;
/* ============================================ */
/* CHARTS (Apexcharts) */
/* ============================================ */
/* IMPORTANT: Avoid using Tailwind's default color variables for gradient Apexcharts colors.
The Apexcharts plugin does not support the oklch color format, so these colors may not render correctly. */
--chart-colors-background-inverse: ...;
--chart-colors-foreground: ...;
--chart-primary: ...;
--chart-colors-primary: ...;
--chart-colors-primary-inverse: ...;
--chart-colors-primary-hex: #......; /* Must be valid hex */
--chart-colors-primary-hex-inverse: #......; /* Must be valid hex */
--chart-1: ...;
--chart-colors-chart-1: ...;
--chart-colors-chart-1-inverse: ...;
--chart-colors-chart-1-hex: #......;
--chart-colors-chart-1-hex-inverse: #......;
--chart-2: ...;
--chart-colors-chart-2: ...;
--chart-colors-chart-2-inverse: ...;
--chart-colors-chart-2-hex: #......;
--chart-colors-chart-2-hex-inverse: #......;
--chart-3: ...;
--chart-colors-chart-3: ...;
--chart-colors-chart-3-inverse: ...;
--chart-colors-chart-3-hex: #......;
--chart-colors-chart-3-hex-inverse: #......;
--chart-4: ...;
--chart-colors-chart-4: ...;
--chart-colors-chart-4-inverse: ...;
--chart-colors-chart-4-hex: #......;
--chart-colors-chart-4-hex-inverse: #......;
--chart-5: ...;
--chart-colors-chart-5: ...;
--chart-colors-chart-5-inverse: ...;
--chart-colors-chart-5-hex: #......;
--chart-colors-chart-5-hex-inverse: #......;
--chart-6: ...;
--chart-colors-chart-6: ...;
--chart-colors-chart-6-inverse: ...;
--chart-colors-chart-6-hex: #......;
--chart-colors-chart-6-hex-inverse: #......;
--chart-7: ...;
--chart-colors-chart-7: ...;
--chart-colors-chart-7-inverse: ...;
--chart-colors-chart-7-hex: #......;
--chart-colors-chart-7-hex-inverse: #......;
--chart-8: ...;
--chart-colors-chart-8: ...;
--chart-colors-chart-8-inverse: ...;
--chart-colors-chart-8-hex: #......;
--chart-colors-chart-8-hex-inverse: #......;
--chart-9: ...;
--chart-colors-chart-9: ...;
--chart-colors-chart-9-inverse: ...;
--chart-colors-chart-9-hex: #......;
--chart-colors-chart-9-hex-inverse: #......;
--chart-10: ...;
--chart-colors-chart-10: ...;
--chart-colors-chart-10-inverse: ...;
--chart-colors-chart-10-hex: #......;
--chart-colors-chart-10-hex-inverse: #......;
--chart-colors-candlestick-upward: ...;
--chart-colors-candlestick-upward-inverse: ...;
--chart-colors-candlestick-downward: ...;
--chart-colors-candlestick-downward-inverse: ...;
--chart-colors-labels: ...;
--chart-colors-labels-inverse: ...;
--chart-colors-xaxis-labels: ...;
--chart-colors-xaxis-labels-inverse: ...;
--chart-colors-yaxis-labels: ...;
--chart-colors-yaxis-labels-inverse: ...;
--chart-colors-grid-border: ...;
--chart-colors-grid-border-inverse: ...;
--chart-colors-bar-ranges: ...;
--chart-colors-bar-ranges-inverse: ...;
/* ============================================ */
/* MAPS (jsvectormap) */
/* ============================================ */
--map-colors-primary: ...;
--map-colors-primary-inverse: ...;
--map-colors-default: ...;
--map-colors-default-inverse: ...;
--map-colors-highlight: ...;
--map-colors-highlight-inverse: ...;
--map-colors-border: ...;
--map-colors-border-inverse: ...;
}
/* ============================================== */
/* DARK MODE OVERRIDES */
/* ============================================== */
[data-theme="theme-<name>"].dark {
/* ============================================ */
/* DARK MODE OVERRIDES */
/* ============================================ */
/*
* CRITICAL: Choose correct mode based on user request
*
* DEFAULT (no "matching dark mode" requested):
* Use Tailwind grayscale (stone/zinc/slate) based on brand warmth:
* - Warm brands (orange/amber/brown/rose) → stone or neutral
* - Cool brands (blue/cyan/teal/indigo) → zinc or slate
*
* IF USER REQUESTED "matching dark mode":
* Use custom gray palette: var(--color-<name>-gray-*)
* (uncomment the dual-palette section below and comment out Tailwind section)
*/
/* -------------------------------------------- */
/* DEFAULT: Use Tailwind gray for dark mode */
/* -------------------------------------------- */
/* BACKGROUNDS - dark end of scale (800-950) */
--background: var(--color-stone-950);
--background-1: var(--color-stone-900);
--background-2: var(--color-stone-800);
--foreground: var(--color-stone-200);
--inverse: var(--color-stone-50);
/* BORDERS - mid-dark range (700-900) */
--border: var(--color-stone-800);
--border-line-inverse: var(--color-stone-200);
--border-line-1: var(--color-stone-900);
--border-line-2: var(--color-stone-850);
--border-line-3: var(--color-stone-800);
--border-line-4: var(--color-stone-750);
--border-line-5: var(--color-stone-700);
--border-line-6: var(--color-stone-650);
--border-line-7: var(--color-stone-600);
--border-line-8: var(--color-stone-550);
/* -------------------------------------------- */
/* DUAL-PALETTE: Use custom gray (uncomment) */
/* -------------------------------------------- */
/*
--background: var(--color-<name>-gray-950);
--background-1: var(--color-<name>-gray-900);
--background-2: var(--color-<name>-gray-800);
--foreground: var(--color-<name>-gray-200);
--inverse: var(--color-<name>-gray-50);
--border: var(--color-<name>-gray-800);
--border-line-inverse: var(--color-<name>-gray-200);
--border-line-1: var(--color-<name>-gray-900);
--border-line-2: var(--color-<name>-gray-850);
... etc ...
*/
/* PRIMARY STATES (adjust for dark backgrounds) */
--primary: var(--color-<name>-400);
/*
* --primary-foreground: DYNAMIC based on brand lightness
* Light brands (yellow/lime/cyan hue 50-110 or 160-195): var(--color-<gray>-900)
* Dark brands (most colors): var(--color-white)
*/
--primary-foreground: var(--color-white); /* or var(--color-<gray>-900) for light brands */
--primary-hover: var(--color-<name>-500);
--primary-focus: var(--color-<name>-500);
--primary-active: var(--color-<name>-500);
--primary-checked: var(--color-<name>-400);
/* SECONDARY */
--secondary: var(--color-white);
--secondary-hover: var(--color-stone-100);
--secondary-focus: var(--color-stone-100);
--secondary-active: ...;
/* LAYER */
--layer: ...;
--layer-line: ...;
--layer-foreground: ...;
--layer-hover: ...;
--layer-focus: ...;
--layer-active: ...;
/* SURFACE */
--surface: ...;
--surface-1: ...;
--surface-2: ...;
--surface-3: ...;
--surface-4: ...;
--surface-5: ...;
--surface-foreground: ...;
--surface-hover: ...;
--surface-focus: ...;
--surface-active: ...;
/* MUTED */
--muted: ...;
--muted-foreground: ...;
--muted-foreground-1: ...;
--muted-foreground-2: ...;
--muted-hover: ...;
--muted-focus: ...;
--muted-active: ...;
/* NAVBAR (all variants) */
--navbar: ...;
--navbar-line: ...;
--navbar-divider: ...;
--navbar-nav-foreground: ...;
--navbar-nav-hover: ...;
--navbar-nav-focus: ...;
--navbar-nav-active: ...;
--navbar-nav-list-divider: ...;
--navbar-inverse: ...;
--navbar-1: ...;
--navbar-1-line: ...;
--navbar-1-divider: ...;
--navbar-1-nav-foreground: ...;
--navbar-1-nav-hover: ...;
--navbar-1-nav-focus: ...;
--navbar-1-nav-active: ...;
--navbar-1-nav-list-divider: ...;
--navbar-2: ...;
--navbar-2-divider: ...;
--navbar-2-nav-foreground: ...;
--navbar-2-nav-hover: ...;
--navbar-2-nav-focus: ...;
--navbar-2-nav-active: ...;
--navbar-2-nav-list-divider: ...;
/* SIDEBAR (all variants) */
--sidebar: ...;
--sidebar-line: ...;
--sidebar-divider: ...;
--sidebar-nav-foreground: ...;
--sidebar-nav-hover: ...;
--sidebar-nav-focus: ...;
--sidebar-nav-active: ...;
--sidebar-nav-list-divider: ...;
--sidebar-inverse: ...;
--sidebar-1: ...;
--sidebar-1-line: ...;
--sidebar-1-divider: ...;
--sidebar-1-nav-foreground: ...;
--sidebar-1-nav-hover: ...;
--sidebar-1-nav-focus: ...;
--sidebar-1-nav-active: ...;
--sidebar-1-nav-list-divider: ...;
--sidebar-2: ...;
--sidebar-2-divider: ...;
--sidebar-2-nav-foreground: ...;
--sidebar-2-nav-hover: ...;
--sidebar-2-nav-focus: ...;
--sidebar-2-nav-active: ...;
--sidebar-2-nav-list-divider: ...;
/* CARD */
--card: ...;
--card-line: ...;
--card-divider: ...;
--card-header: ...;
--card-footer: ...;
--card-inverse: ...;
/* DROPDOWN */
--dropdown: ...;
--dropdown-1: ...;
--dropdown-divider: ...;
--dropdown-header: ...;
--dropdown-footer: ...;
--dropdown-item-foreground: ...;
--dropdown-item-hover: ...;
--dropdown-item-focus: ...;
--dropdown-item-active: ...;
--dropdown-inverse: ...;
/* SELECT */
--select: ...;
--select-1: ...;
--select-item-foreground: ...;
--select-item-hover: ...;
--select-item-focus: ...;
--select-item-active: ...;
--select-inverse: ...;
/* OVERLAY */
--overlay: ...;
--overlay-divider: ...;
--overlay-header: ...;
--overlay-footer: ...;
--overlay-inverse: ...;
/* POPOVER, TOOLTIP, TABLE */
--popover: ...;
--popover-line: ...;
--tooltip: ...;
--tooltip-foreground: ...;
--table-line: ...;
/* SWITCH */
--switch: ...;
/* FOOTER */
--footer: ...;
--footer-line: ...;
--footer-inverse: ...;
/* SCROLLBAR */
--scrollbar-track: ...;
--scrollbar-thumb: ...;
--scrollbar-track-inverse: ...;
--scrollbar-thumb-inverse: ...;
/* CHARTS (dark mode adjustments) */
--chart-primary: ...;
--chart-5: ...; /* Often needs adjustment */
--chart-6: ...; /* Often needs adjustment */
--chart-8: ...;
--chart-9: ...;
--chart-10: ...;
}
---
Enable Theme Snippet
Provide this snippet to users for enabling the theme:
CSS (in tailwind.css or main entry)
@import "tailwindcss";
@import "./themes/theme.css";
@import "./themes/<name>.css";HTML
<html data-theme="theme-<name>">
...
</html>For dark mode, add the .dark class:
<html data-theme="theme-<name>" class="dark">
...
</html>---
Notes
Palette Rules: 1. @theme theme-<name> inline { } ALWAYS contains TWO palettes: brand + gray 2. Light mode ALWAYS uses custom gray palette for backgrounds/surfaces/borders 3. Dark mode DEFAULT: Use Tailwind gray (stone/zinc) based on brand warmth 4. Dark mode if "matching dark mode" requested: Use custom gray palette 5. Brand palette is VIBRANT by default; only use soft/low-chroma if user says "muted/ash/soft" 6. Gray palette uses bell curve chroma (low at 800-950 for clean dark mode)
Token Rules: 7. All semantic tokens (--background, --primary, etc.) go in selector blocks, NOT in @theme block 8. Replace all ... with actual color values using var(--color-<family>-<shade>) format 9. Chart -hex tokens MUST contain valid hex values (e.g., #2563eb), not CSS variables 10. Dark mode MUST use CSS variables for readability - never hardcode oklch values 11. Use ONE consistent grayscale family throughout dark mode (don't mix) 12. Dark mode only needs to override tokens that differ from light mode
Theme Generator QA (Self-Check)
Use this checklist immediately before returning a generated theme.
---
1) Structural correctness
- Did I include BOTH imports at the top?
@import "tailwindcss";@import "./theme.css";- Did I use the correct theme name everywhere (
theme-<name>)? - Did I include the theme scoping block with custom palettes?
@theme theme-<name> inline { /* brand + gray palettes */ }- Did I include BOTH selectors?
- Light:
:root[data-theme="theme-<name>"], [data-theme="theme-<name>"] - Dark:
[data-theme="theme-<name>"].dark
---
2) Mapping / HTML safety (critical)
- Did I put ONLY custom color palettes inside
@themeblock? - The
@theme theme-<name> inline { }block should ONLY contain: - Brand palette:
--color-<name>-*(50-950) - Gray palette:
--color-<name>-gray-*(50-950) - Semantic tokens (
--background,--primary, etc.) go in SELECTOR blocks, NOT inside @theme. - Theme files must NOT change Tailwind mappings.
- Did I introduce any change that requires users to edit HTML utility classes?
- If yes: remove it. Themes must activate via
data-themeonly.
---
3) Full theme coverage sanity
3.1 Global surfaces + text
- Did I define background tokens?
--background,--background-1,--background-2,--background-plain- Did I define foreground tokens?
--foreground,--foreground-inverse,--inverse
3.2 Borders (full scale)
- Did I define the base border token?
--border- Did I define the full border scale?
--border-line-inverse--border-line-1through--border-line-8- Is the border scale coherent and usable across different surfaces?
3.3 Primary ramp + states
- Did I define the full primary color ramp?
--primary-50through--primary-950(all 11 shades)- Do the shades feel like one cohesive family?
- Did I define all primary state tokens?
--primary,--primary-hover,--primary-focus,--primary-active,--primary-checked--primary-line,--primary-foreground- Is
--primary-foregroundreadable against--primary?
3.4 Secondary / Layer / Surface / Muted / Destructive
- Did I define secondary tokens?
--secondary,--secondary-line,--secondary-foreground--secondary-hover,--secondary-focus,--secondary-active- Did I define layer tokens?
--layer,--layer-line,--layer-foreground--layer-hover,--layer-focus,--layer-active- Did I define surface tokens?
--surface,--surface-1through--surface-5--surface-line,--surface-foreground--surface-hover,--surface-focus,--surface-active- Did I define muted tokens?
--muted,--muted-foreground,--muted-foreground-1,--muted-foreground-2--muted-hover,--muted-focus,--muted-active- Did I define destructive tokens?
--destructive,--destructive-foreground--destructive-hover,--destructive-focus- Is destructive visually distinct from primary?
3.5 Component groups (all variants)
- Did I define navbar tokens (base + variants)?
--navbar,--navbar-line,--navbar-divider,--navbar-inverse--navbar-nav-foreground,--navbar-nav-hover,--navbar-nav-focus,--navbar-nav-active--navbar-nav-list-divider- Same pattern for
--navbar-1-*and--navbar-2-* - Did I define sidebar tokens (base + variants)?
- Same pattern as navbar for
--sidebar-*,--sidebar-1-*,--sidebar-2-* - Did I define card tokens?
--card,--card-line,--card-divider,--card-header,--card-footer,--card-inverse- Did I define dropdown tokens?
--dropdown,--dropdown-1,--dropdown-line,--dropdown-divider--dropdown-header,--dropdown-footer--dropdown-item-foreground,--dropdown-item-hover,--dropdown-item-focus,--dropdown-item-active--dropdown-inverse- Did I define select tokens?
--select,--select-1,--select-line--select-item-foreground,--select-item-hover,--select-item-focus,--select-item-active--select-inverse- Did I define overlay tokens?
--overlay,--overlay-line,--overlay-divider--overlay-header,--overlay-footer,--overlay-inverse- Did I define popover, tooltip, table, switch, footer tokens?
--popover,--popover-line--tooltip,--tooltip-foreground,--tooltip-line--table-line--switch--footer,--footer-line,--footer-inverse- Did I define scrollbar tokens?
--scrollbar-track,--scrollbar-thumb--scrollbar-track-inverse,--scrollbar-thumb-inverse
---
4) Chart / Map token safety
- If I included chart tokens, did I keep
-hexvariants as valid hex values? - Apexcharts does NOT support oklch format
- Did I include the comment warning about oklch incompatibility?
- Did I define chart tokens coherently?
--chart-primary,--chart-1through--chart-10--chart-colors-*variants with-hexand-inversewhere needed- If I included map tokens, are they using compatible color formats?
--map-colors-primary,--map-colors-default,--map-colors-highlight,--map-colors-border- Plus
-inversevariants
---
5) Dark mode completeness
- Did I override all tokens that need different values in dark mode?
- Did I maintain readable contrast in dark mode?
- Text on backgrounds
- Primary foreground on primary background
- Are hover/focus/active states still distinguishable in dark mode?
- Did I use appropriate dark neutral family (e.g.,
stone,neutral,slate)?
---
6) CSS syntax validation
- Are all braces balanced?
- Are all selectors valid?
- Are there any broken or unclosed comments?
- Are there any duplicate conflicting selectors?
- Did I properly close both the light mode and dark mode selector blocks?
---
7) Behavior overrides (if any)
- If I added radius or other behavior overrides, are they theme-scoped?
- Must be inside
@layer utilitieswith theme selector - Did I avoid global overrides that affect non-theme pages?
---
Quick validation summary
Before returning, confirm all are true:
- [ ] Imports present and ordered correctly
- [ ] Theme key is
theme-<name>everywhere - [ ]
@theme theme-<name> inline { }block contains brand + gray palettes - [ ] Light selector:
:root[data-theme="theme-<name>"], [data-theme="theme-<name>"] - [ ] Dark selector:
[data-theme="theme-<name>"].dark - [ ] Only color palettes inside @theme block (NO semantic tokens)
- [ ] Full coverage: backgrounds, foregrounds, borders, primary ramp, states
- [ ] Full coverage: secondary, layer, surface, muted, destructive
- [ ] Full coverage: navbar (3 variants), sidebar (3 variants), card, dropdown, select, overlay
- [ ] Full coverage: popover, tooltip, table, switch, footer, scrollbar
- [ ] Chart/map tokens use hex for
-hexvariants (if included) - [ ] Dark mode overrides are complete and readable
- [ ] CSS syntax is valid
- [ ] No HTML utility class changes required
{
"name": "<theme-name>",
"hue": 200,
"style": "vibrant",
"useCustomDarkGray": false,
"tailwindGray": "neutral"
}# ============================================
# Preline Theme Config Example
# ============================================
name: <theme-name>
brand:
# Option 1: Use hue (0-360)
hue: 200
# Option 2: Use hex color
# primaryColor: "#2F6BFF"
# vibrant (default) or soft
style: vibrant
darkMode:
useCustomGray: false
tailwindGray: neutral
# typography:
# fontSans: "Inter, sans-serif"
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const TRUSTED_THEME_DIRS = [
'src/assets/css/themes',
'src/css/themes',
'src/styles/themes',
'assets/css/themes',
'styles/themes',
'dist/assets/css/themes',
'npm/preline/css/themes',
];
function parseArgs(argv) {
const options = {
all: false,
absolute: false,
cwd: process.cwd(),
};
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
if (arg === '--all') {
options.all = true;
continue;
}
if (arg === '--absolute') {
options.absolute = true;
continue;
}
if (arg === '--cwd') {
const value = argv[i + 1];
if (!value) {
throw new Error('Missing value for --cwd');
}
options.cwd = path.resolve(value);
i += 1;
continue;
}
if (arg === '--help') {
console.log(`Usage:
node scripts/find-themes-dir.js [--all] [--absolute] [--cwd <path>]
Checks a short allowlist of trusted Preline theme directories and prints matches.
`);
process.exit(0);
}
throw new Error(`Unknown argument: ${arg}`);
}
return options;
}
function hasBaseTheme(dir) {
return fs.existsSync(path.join(dir, 'theme.css'));
}
function main() {
const options = parseArgs(process.argv.slice(2));
const matches = TRUSTED_THEME_DIRS
.map((relativeDir) => {
const absoluteDir = path.resolve(options.cwd, relativeDir);
return {
relativeDir,
absoluteDir,
};
})
.filter(({ absoluteDir }) => hasBaseTheme(absoluteDir));
if (matches.length === 0) {
process.exit(1);
}
const output = options.all ? matches : [matches[0]];
output.forEach(({ relativeDir, absoluteDir }) => {
console.log(options.absolute ? absoluteDir : relativeDir);
});
}
try {
main();
} catch (error) {
console.error(error.message);
process.exit(1);
}
/**
* Preline Theme Generator
*
* Core library for generating a complete theme CSS file from config.
*
* Use `run-theme-generator.js` for CLI execution. Import this file when you
* need the generator programmatically:
*
* const { generateTheme } = require('./generate-theme.js');
* const css = generateTheme(config);
*/
// ============================================
// COLOR CONVERSION UTILITIES
// ============================================
/**
* Convert OKLCH to sRGB
* Based on CSS Color Level 4 spec
*/
function oklchToRgb(l, c, h) {
// Convert to OKLab first
const hRad = (h * Math.PI) / 180;
const a = c * Math.cos(hRad);
const b = c * Math.sin(hRad);
// OKLab to linear sRGB via LMS
const L = l + 0.3963377774 * a + 0.2158037573 * b;
const M = l - 0.1055613458 * a - 0.0638541728 * b;
const S = l - 0.0894841775 * a - 1.2914855480 * b;
const l_ = L * L * L;
const m_ = M * M * M;
const s_ = S * S * S;
let rLinear = 4.0767416621 * l_ - 3.3077115913 * m_ + 0.2309699292 * s_;
let gLinear = -1.2684380046 * l_ + 2.6097574011 * m_ - 0.3413193965 * s_;
let bLinear = -0.0041960863 * l_ - 0.7034186147 * m_ + 1.7076147010 * s_;
// Linear to sRGB gamma
const gammaCorrect = (x) => {
if (x >= 0.0031308) {
return 1.055 * Math.pow(x, 1 / 2.4) - 0.055;
}
return 12.92 * x;
};
let r = Math.round(Math.max(0, Math.min(1, gammaCorrect(rLinear))) * 255);
let g = Math.round(Math.max(0, Math.min(1, gammaCorrect(gLinear))) * 255);
let bVal = Math.round(Math.max(0, Math.min(1, gammaCorrect(bLinear))) * 255);
return { r, g, b: bVal };
}
/**
* Convert OKLCH to hex color
* @param {number} l - Lightness (0-100 as percentage, will be converted to 0-1)
* @param {number} c - Chroma (0-0.4 typically)
* @param {number} h - Hue (0-360)
* @returns {string} Hex color string (e.g., "#2563eb")
*/
function oklchToHex(l, c, h) {
// Convert percentage to 0-1 if needed
const lightness = l > 1 ? l / 100 : l;
const { r, g, b } = oklchToRgb(lightness, c, h);
const toHex = (n) => {
const hex = Math.max(0, Math.min(255, n)).toString(16);
return hex.length === 1 ? '0' + hex : hex;
};
return `#${toHex(r)}${toHex(g)}${toHex(b)}`;
}
/**
* Convert sRGB channel value to linear space
* @param {number} channel - 0-255
* @returns {number}
*/
function srgbChannelToLinear(channel) {
const normalized = channel / 255;
if (normalized <= 0.04045) {
return normalized / 12.92;
}
return Math.pow((normalized + 0.055) / 1.055, 2.4);
}
/**
* Relative luminance per WCAG
* @param {{r: number, g: number, b: number}} rgb
* @returns {number}
*/
function getRelativeLuminance(rgb) {
const r = srgbChannelToLinear(rgb.r);
const g = srgbChannelToLinear(rgb.g);
const b = srgbChannelToLinear(rgb.b);
return (0.2126 * r) + (0.7152 * g) + (0.0722 * b);
}
/**
* WCAG contrast ratio between two colors
* @param {{r: number, g: number, b: number}} colorA
* @param {{r: number, g: number, b: number}} colorB
* @returns {number}
*/
function getContrastRatio(colorA, colorB) {
const luminanceA = getRelativeLuminance(colorA);
const luminanceB = getRelativeLuminance(colorB);
const lighter = Math.max(luminanceA, luminanceB);
const darker = Math.min(luminanceA, luminanceB);
return (lighter + 0.05) / (darker + 0.05);
}
/**
* Convert hex color to hue (0-360)
* @param {string} hex - Hex color (e.g., "#2F6BFF" or "2F6BFF")
* @returns {number} Hue in degrees (0-360)
*/
function hexToHue(hex) {
// Remove # if present
hex = hex.replace(/^#/, '');
const r = parseInt(hex.substring(0, 2), 16) / 255;
const g = parseInt(hex.substring(2, 4), 16) / 255;
const b = parseInt(hex.substring(4, 6), 16) / 255;
const max = Math.max(r, g, b);
const min = Math.min(r, g, b);
const delta = max - min;
let h = 0;
if (delta !== 0) {
if (max === r) {
h = ((g - b) / delta) % 6;
} else if (max === g) {
h = (b - r) / delta + 2;
} else {
h = (r - g) / delta + 4;
}
h = Math.round(h * 60);
if (h < 0) h += 360;
}
return h;
}
// ============================================
// VALIDATION
// ============================================
/**
* Validate config and return error messages if invalid
* @param {Object} config
* @returns {string[]} Array of error messages (empty if valid)
*/
function validateConfig(config) {
const errors = [];
// Required: name
if (!config.name || typeof config.name !== 'string') {
errors.push('Missing required field: "name" (string, kebab-case theme name)');
} else if (!/^[a-z][a-z0-9-]*$/.test(config.name)) {
errors.push(`Invalid name "${config.name}": must be kebab-case (lowercase letters, numbers, hyphens, starting with letter)`);
}
// Required: hue OR primaryColor
const hasHue = typeof config.hue === 'number';
const hasPrimaryColor = typeof config.primaryColor === 'string';
if (!hasHue && !hasPrimaryColor) {
errors.push('Missing required field: "hue" (number 0-360) or "primaryColor" (hex string like "#2F6BFF")');
}
if (hasHue && (config.hue < 0 || config.hue > 360)) {
errors.push(`Invalid hue ${config.hue}: must be between 0 and 360`);
}
if (hasPrimaryColor && !/^#?[0-9A-Fa-f]{6}$/.test(config.primaryColor)) {
errors.push(`Invalid primaryColor "${config.primaryColor}": must be 6-digit hex (e.g., "#2F6BFF")`);
}
// Optional: style
if (config.style && !['vibrant', 'soft'].includes(config.style)) {
errors.push(`Invalid style "${config.style}": must be "vibrant" or "soft"`);
}
// Optional: tailwindGray
const validGrays = ['neutral', 'stone', 'zinc', 'slate', 'gray'];
if (config.tailwindGray && !validGrays.includes(config.tailwindGray)) {
errors.push(`Invalid tailwindGray "${config.tailwindGray}": must be one of ${validGrays.join(', ')}`);
}
return errors;
}
// ============================================
// PALETTE GENERATION
// ============================================
/**
* Calculate lightness for a shade using a formula-based approach
* @param {number} shade - Shade number (50-950)
* @returns {number} Lightness percentage
*/
function calculateLightness(shade) {
// Use a polynomial curve that matches Tailwind's visual rhythm
// 50 = ~97%, 500 = ~60%, 950 = ~20%
const shadeNormalized = shade / 1000;
const lightness = 97 - (shadeNormalized * 85) + (Math.pow(shadeNormalized, 2) * 10);
return Math.round(Math.max(15, Math.min(98, lightness)) * 10) / 10;
}
/**
* Generate brand color palette (vibrant or soft)
*/
function generateBrandPalette(name, hue, style = 'vibrant') {
const shades = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950];
// Chroma values - vibrant vs soft
const chromaVibrant = {
50: 0.08, 100: 0.10, 200: 0.12, 300: 0.14, 400: 0.16,
500: 0.14, 600: 0.12, 700: 0.10, 800: 0.08, 900: 0.06, 950: 0.05
};
const chromaSoft = {
50: 0.012, 100: 0.020, 200: 0.035, 300: 0.055, 400: 0.075,
500: 0.085, 600: 0.080, 700: 0.070, 800: 0.055, 900: 0.040, 950: 0.030
};
const chroma = style === 'soft' ? chromaSoft : chromaVibrant;
return shades.map(shade => {
const l = calculateLightness(shade);
const c = chroma[shade];
return ` --color-${name}-${shade}: oklch(${l}% ${c} ${hue});`;
}).join('\n');
}
/**
* Generate gray palette with bell curve chroma
* Low at extremes (for clean light/dark backgrounds), peak at midtones
*/
function generateGrayPalette(name, hue) {
const shades = [
{ shade: 50, l: 98, c: 0.002 },
{ shade: 100, l: 95.5, c: 0.004 },
{ shade: 200, l: 89.7, c: 0.008 },
{ shade: 300, l: 82.7, c: 0.012 },
{ shade: 400, l: 73, c: 0.018 },
{ shade: 500, l: 62.5, c: 0.020 }, // Peak chroma
{ shade: 600, l: 52.8, c: 0.016 },
{ shade: 700, l: 41.4, c: 0.012 },
{ shade: 800, l: 26.9, c: 0.006 }, // Darker - matches Tailwind neutral-800
{ shade: 900, l: 20.5, c: 0.004 }, // Darker - matches Tailwind neutral-900
{ shade: 950, l: 14.1, c: 0.002 }, // Darker - matches Tailwind neutral-950
];
return shades.map(({ shade, l, c }) =>
` --color-${name}-gray-${shade}: oklch(${l}% ${c} ${hue});`
).join('\n');
}
function getBrandShadeOklch(style, hue, shade) {
const chromaByStyle = {
vibrant: {
50: 0.08, 100: 0.10, 200: 0.12, 300: 0.14, 400: 0.16,
500: 0.14, 600: 0.12, 700: 0.10, 800: 0.08, 900: 0.06, 950: 0.05
},
soft: {
50: 0.012, 100: 0.020, 200: 0.035, 300: 0.055, 400: 0.075,
500: 0.085, 600: 0.080, 700: 0.070, 800: 0.055, 900: 0.040, 950: 0.030
}
};
return {
l: calculateLightness(shade),
c: chromaByStyle[style][shade],
h: hue,
};
}
function getThemeGrayShadeOklch(hue, shade) {
const grayScale = {
50: { l: 98, c: 0.002 },
100: { l: 95.5, c: 0.004 },
200: { l: 89.7, c: 0.008 },
300: { l: 82.7, c: 0.012 },
400: { l: 73, c: 0.018 },
500: { l: 62.5, c: 0.020 },
600: { l: 52.8, c: 0.016 },
700: { l: 41.4, c: 0.012 },
800: { l: 26.9, c: 0.006 },
900: { l: 20.5, c: 0.004 },
950: { l: 14.1, c: 0.002 },
};
return {
...grayScale[shade],
h: hue,
};
}
function oklchColorToRgb(color) {
return oklchToRgb(color.l / 100, color.c, color.h);
}
function getWorstContrast(candidate, backgrounds) {
return Math.min(...backgrounds.map((background) => getContrastRatio(candidate, background)));
}
function pickContrastingForegroundValue(candidates, backgrounds) {
const evaluated = candidates.map((candidate) => ({
value: candidate.value,
worstContrast: getWorstContrast(candidate.rgb, backgrounds),
}));
evaluated.sort((left, right) => right.worstContrast - left.worstContrast);
return evaluated[0].value;
}
// ============================================
// CHART & MAP TOKEN GENERATION
// ============================================
function buildChartSeriesTokens(series) {
return series.map((item, index) => {
const num = index + 1;
return `
--chart-${num}: ${item.base};
--chart-colors-chart-${num}: ${item.base};
--chart-colors-chart-${num}-inverse: ${item.inverse};
--chart-colors-chart-${num}-hex: ${item.base};
--chart-colors-chart-${num}-hex-inverse: ${item.inverse};`;
}).join('');
}
function generateChartTokensLight(name) {
const g = `--color-${name}-gray`;
const series = [
{ base: 'var(--color-primary-50)', inverse: 'var(--color-primary-100)' },
{ base: 'var(--color-primary-200)', inverse: 'var(--color-primary-300)' },
{ base: 'var(--color-primary-400)', inverse: 'var(--color-primary-500)' },
{ base: 'var(--color-primary-700)', inverse: 'var(--color-primary-500)' },
{ base: 'var(--color-primary-900)', inverse: 'var(--color-primary-700)' },
{ base: 'var(--color-sky-600)', inverse: 'var(--color-sky-400)' },
{ base: 'var(--color-emerald-600)', inverse: 'var(--color-emerald-400)' },
{ base: 'var(--color-violet-600)', inverse: 'var(--color-violet-400)' },
{ base: 'var(--color-rose-500)', inverse: 'var(--color-rose-300)' },
{ base: `var(${g}-300)`, inverse: `var(${g}-500)` },
];
return `
/* ============================================ */
/* CHARTS (Apexcharts) */
/* ============================================ */
--chart-colors-background: var(--background-plain);
--chart-colors-background-inverse: var(--inverse);
--chart-colors-chart-inverse: var(--background-1);
--chart-colors-foreground: var(--foreground);
--chart-colors-foreground-inverse: var(--foreground-inverse);
--chart-primary: var(--color-primary-600);
--chart-colors-primary: var(--color-primary-600);
--chart-colors-primary-inverse: var(--color-primary-300);
--chart-colors-primary-hex: var(--color-primary-600);
--chart-colors-primary-hex-inverse: var(--color-primary-300);` +
buildChartSeriesTokens(series) +
`
--chart-colors-candlestick-upward: var(--color-green-500);
--chart-colors-candlestick-upward-inverse: var(--color-green-400);
--chart-colors-candlestick-downward: var(--color-red-500);
--chart-colors-candlestick-downward-inverse: var(--color-red-400);
--chart-colors-labels: var(${g}-600);
--chart-colors-labels-inverse: var(${g}-400);
--chart-colors-xaxis-labels: var(${g}-500);
--chart-colors-xaxis-labels-inverse: var(${g}-400);
--chart-colors-yaxis-labels: var(${g}-500);
--chart-colors-yaxis-labels-inverse: var(${g}-400);
--chart-colors-grid-border: var(--border);
--chart-colors-grid-border-inverse: var(${g}-700);
--chart-colors-bar-ranges: var(--surface-1);
--chart-colors-bar-ranges-inverse: var(${g}-700);`;
}
function generateChartTokensDark(name, grayVar, darkModePrimaryProfile) {
const primaryShade = darkModePrimaryProfile.primaryShade;
const interactionShade = darkModePrimaryProfile.interactionShade;
const companionShade = primaryShade >= 500 ? 300 : 200;
const series = [
{ base: 'var(--color-primary-200)', inverse: 'var(--color-primary-300)' },
{ base: 'var(--color-primary-300)', inverse: 'var(--color-primary-400)' },
{ base: 'var(--color-primary-400)', inverse: `var(--color-primary-${primaryShade})` },
{ base: `var(--color-primary-${primaryShade})`, inverse: `var(--color-primary-${interactionShade})` },
{ base: `var(--color-primary-${companionShade})`, inverse: `var(--color-primary-${primaryShade})` },
{ base: 'var(--color-sky-400)', inverse: 'var(--color-sky-300)' },
{ base: 'var(--color-emerald-400)', inverse: 'var(--color-emerald-300)' },
{ base: 'var(--color-violet-400)', inverse: 'var(--color-violet-300)' },
{ base: 'var(--color-rose-400)', inverse: 'var(--color-rose-300)' },
{ base: `var(${grayVar}-300)`, inverse: `var(${grayVar}-500)` },
];
return `
/* CHARTS - dark mode adjustments */
--chart-colors-background: var(--background);
--chart-colors-background-inverse: var(${grayVar}-100);
--chart-colors-chart-inverse: var(--background-1);
--chart-colors-foreground: var(${grayVar}-300);
--chart-colors-foreground-inverse: var(--color-white);
--chart-primary: var(--color-primary-${primaryShade});
--chart-colors-primary: var(--color-primary-${primaryShade});
--chart-colors-primary-inverse: var(--color-primary-${companionShade});
--chart-colors-primary-hex: var(--color-primary-${primaryShade});
--chart-colors-primary-hex-inverse: var(--color-primary-${companionShade});` +
buildChartSeriesTokens(series) +
`
--chart-colors-labels: var(${grayVar}-400);
--chart-colors-xaxis-labels: var(${grayVar}-400);
--chart-colors-yaxis-labels: var(${grayVar}-400);
--chart-colors-grid-border: var(--border);
--chart-colors-bar-ranges: var(--surface);`;
}
function generateMapTokens(name) {
const g = `--color-${name}-gray`;
return `
/* ============================================ */
/* MAPS (jsvectormap) */
/* ============================================ */
--map-colors-primary: var(--color-primary-500);
--map-colors-primary-inverse: var(--color-primary-300);
--map-colors-default: var(--surface-1);
--map-colors-default-inverse: var(${g}-700);
--map-colors-highlight: var(--color-primary-300);
--map-colors-highlight-inverse: var(--color-primary-500);
--map-colors-border: var(--border-line-3);
--map-colors-border-inverse: var(${g}-600);`;
}
// ============================================
// HELPER FUNCTIONS
// ============================================
function getPrimaryForegroundForLightMode(name, hue, style) {
const backgrounds = [600, 700]
.map((shade) => getBrandShadeOklch(style, hue, shade))
.map(oklchColorToRgb);
const darkText = oklchColorToRgb(getThemeGrayShadeOklch(hue, 900));
return pickContrastingForegroundValue([
{ value: 'var(--color-white)', rgb: { r: 255, g: 255, b: 255 } },
{ value: `var(--color-${name}-gray-900)`, rgb: darkText },
], backgrounds);
}
function getDarkModePrimaryProfile(name, hue, style, grayVar) {
const white = { r: 255, g: 255, b: 255 };
const darkText = oklchColorToRgb(getThemeGrayShadeOklch(hue, 900));
const minimumUiContrast = 3;
const profiles = [
{
key: 'white',
foregroundValue: 'var(--color-white)',
switchValue: 'var(--color-white)',
primaryShade: 500,
interactionShade: 600,
checkedShade: 500,
backgrounds: [500, 600].map((shade) => oklchColorToRgb(getBrandShadeOklch(style, hue, shade))),
foregroundRgb: white,
},
{
key: 'dark',
foregroundValue: `var(${grayVar}-900)`,
switchValue: `var(${grayVar}-800)`,
primaryShade: 400,
interactionShade: 500,
checkedShade: 400,
backgrounds: [400, 500].map((shade) => oklchColorToRgb(getBrandShadeOklch(style, hue, shade))),
foregroundRgb: darkText,
},
].map((profile) => ({
...profile,
worstContrast: getWorstContrast(profile.foregroundRgb, profile.backgrounds),
}));
const preferredWhiteProfile = profiles.find((profile) =>
profile.key === 'white' && profile.worstContrast >= minimumUiContrast
);
if (preferredWhiteProfile) {
return preferredWhiteProfile;
}
const acceptableProfiles = profiles.filter((profile) => profile.worstContrast >= minimumUiContrast);
if (acceptableProfiles.length > 0) {
acceptableProfiles.sort((left, right) => right.worstContrast - left.worstContrast);
return acceptableProfiles[0];
}
profiles.sort((left, right) => right.worstContrast - left.worstContrast);
return profiles[0];
}
// ============================================
// THEME SECTION GENERATORS
// ============================================
function generateLightModeTokens(name, hue, style) {
const g = `--color-${name}-gray`;
const primaryForeground = getPrimaryForegroundForLightMode(name, hue, style);
return `
/* ============================================ */
/* GLOBAL SURFACES + TEXT */
/* ============================================ */
--background: var(--color-white);
--background-1: var(${g}-50);
--background-2: var(${g}-100);
--background-plain: var(--color-white);
--foreground: var(${g}-800);
--foreground-inverse: var(--color-white);
--inverse: var(--color-primary-950);
/* ============================================ */
/* BORDERS (Full Scale) */
/* ============================================ */
--border: var(${g}-200);
--border-line-inverse: var(--color-white);
--border-line-1: var(${g}-100);
--border-line-2: var(${g}-200);
--border-line-3: var(${g}-300);
--border-line-4: var(${g}-400);
--border-line-5: var(${g}-500);
--border-line-6: var(${g}-600);
--border-line-7: var(${g}-700);
--border-line-8: var(${g}-800);
/* ============================================ */
/* PRIMARY RAMP (Full 11-shade scale) */
/* ============================================ */
--primary-50: var(--color-${name}-50);
--primary-100: var(--color-${name}-100);
--primary-200: var(--color-${name}-200);
--primary-300: var(--color-${name}-300);
--primary-400: var(--color-${name}-400);
--primary-500: var(--color-${name}-500);
--primary-600: var(--color-${name}-600);
--primary-700: var(--color-${name}-700);
--primary-800: var(--color-${name}-800);
--primary-900: var(--color-${name}-900);
--primary-950: var(--color-${name}-950);
/* PRIMARY STATES */
--primary: var(--color-primary-600);
--primary-line: transparent;
--primary-foreground: ${primaryForeground};
--primary-hover: var(--color-primary-700);
--primary-focus: var(--color-primary-700);
--primary-active: var(--color-primary-700);
--primary-checked: var(--color-primary-600);
/* ============================================ */
/* SECONDARY */
/* ============================================ */
--secondary: var(${g}-900);
--secondary-line: transparent;
--secondary-foreground: var(--color-white);
--secondary-hover: var(${g}-800);
--secondary-focus: var(${g}-800);
--secondary-active: var(${g}-800);
/* ============================================ */
/* LAYER */
/* ============================================ */
--layer: var(--background-plain);
--layer-line: var(--border);
--layer-foreground: var(--foreground);
--layer-hover: var(--background-1);
--layer-focus: var(--background-1);
--layer-active: var(--background-1);
/* ============================================ */
/* SURFACE */
/* ============================================ */
--surface: var(${g}-100);
--surface-1: var(${g}-200);
--surface-2: var(${g}-300);
--surface-3: var(${g}-400);
--surface-4: var(${g}-500);
--surface-5: var(${g}-600);
--surface-line: transparent;
--surface-foreground: var(${g}-800);
--surface-hover: var(${g}-200);
--surface-focus: var(${g}-200);
--surface-active: var(${g}-200);
/* ============================================ */
/* MUTED */
/* ============================================ */
--muted: var(${g}-50);
--muted-foreground: var(${g}-500);
--muted-foreground-1: var(${g}-600);
--muted-foreground-2: var(${g}-700);
--muted-hover: var(${g}-100);
--muted-focus: var(${g}-100);
--muted-active: var(${g}-100);
/* ============================================ */
/* DESTRUCTIVE */
/* ============================================ */
--destructive: var(--color-red-500);
--destructive-foreground: var(--color-white);
--destructive-hover: var(--color-red-600);
--destructive-focus: var(--color-red-600);
/* ============================================ */
/* NAVBAR */
/* ============================================ */
--navbar: var(--background-plain);
--navbar-line: var(--border);
--navbar-divider: var(--border);
--navbar-nav-foreground: var(--foreground);
--navbar-nav-hover: var(--muted-hover);
--navbar-nav-focus: var(--muted-hover);
--navbar-nav-active: var(--muted-hover);
--navbar-nav-list-divider: var(--border);
--navbar-inverse: var(--inverse);
--navbar-1: var(--background-1);
--navbar-1-line: var(--border);
--navbar-1-divider: var(--border);
--navbar-1-nav-foreground: var(--foreground);
--navbar-1-nav-hover: var(--surface-1);
--navbar-1-nav-focus: var(--surface-1);
--navbar-1-nav-active: var(--surface-1);
--navbar-1-nav-list-divider: var(--border);
--navbar-2: var(--background-2);
--navbar-2-line: transparent;
--navbar-2-divider: var(--border-line-3);
--navbar-2-nav-foreground: var(--foreground);
--navbar-2-nav-hover: var(--surface-1);
--navbar-2-nav-focus: var(--surface-1);
--navbar-2-nav-active: var(--surface-1);
--navbar-2-nav-list-divider: var(--border);
/* ============================================ */
/* SIDEBAR */
/* ============================================ */
--sidebar: var(--background-plain);
--sidebar-line: var(--border);
--sidebar-divider: var(--border);
--sidebar-nav-foreground: var(--foreground);
--sidebar-nav-hover: var(--muted-hover);
--sidebar-nav-focus: var(--muted-hover);
--sidebar-nav-active: var(--muted-hover);
--sidebar-nav-list-divider: var(--border);
--sidebar-inverse: var(--inverse);
--sidebar-1: var(--background-1);
--sidebar-1-line: var(--border);
--sidebar-1-divider: var(--border);
--sidebar-1-nav-foreground: var(--foreground);
--sidebar-1-nav-hover: var(--surface-1);
--sidebar-1-nav-focus: var(--surface-1);
--sidebar-1-nav-active: var(--surface-1);
--sidebar-1-nav-list-divider: var(--border);
--sidebar-2: var(--background-2);
--sidebar-2-line: transparent;
--sidebar-2-divider: var(--border);
--sidebar-2-nav-foreground: var(--foreground);
--sidebar-2-nav-hover: var(--surface-1);
--sidebar-2-nav-focus: var(--surface-1);
--sidebar-2-nav-active: var(--surface-1);
--sidebar-2-nav-list-divider: var(--border);
/* ============================================ */
/* CARD */
/* ============================================ */
--card: var(--background-plain);
--card-line: var(--border);
--card-divider: var(--border);
--card-header: var(--background-2);
--card-footer: var(--background-2);
--card-inverse: var(--inverse);
/* ============================================ */
/* DROPDOWN */
/* ============================================ */
--dropdown: var(--background-plain);
--dropdown-1: var(--background-plain);
--dropdown-line: transparent;
--dropdown-divider: var(--border);
--dropdown-header: var(--background-2);
--dropdown-footer: var(--background-2);
--dropdown-item-foreground: var(--foreground);
--dropdown-item-hover: var(--muted-hover);
--dropdown-item-focus: var(--muted-hover);
--dropdown-item-active: var(--muted-hover);
--dropdown-inverse: var(--inverse);
/* ============================================ */
/* SELECT */
/* ============================================ */
--select: var(--background-plain);
--select-1: var(--background-plain);
--select-line: transparent;
--select-item-foreground: var(--foreground);
--select-item-hover: var(--muted-hover);
--select-item-focus: var(--muted-hover);
--select-item-active: var(--muted-hover);
--select-inverse: var(--inverse);
/* ============================================ */
/* OVERLAY */
/* ============================================ */
--overlay: var(--background-plain);
--overlay-line: transparent;
--overlay-divider: var(--border);
--overlay-header: var(--background-2);
--overlay-footer: var(--background-2);
--overlay-inverse: var(--inverse);
/* ============================================ */
/* POPOVER */
/* ============================================ */
--popover: var(--background-plain);
--popover-line: var(--border-line-1);
/* ============================================ */
/* TOOLTIP */
/* ============================================ */
--tooltip: var(--inverse);
--tooltip-foreground: var(--foreground-inverse);
--tooltip-line: transparent;
/* ============================================ */
/* TABLE */
/* ============================================ */
--table-line: var(--border);
/* ============================================ */
/* SWITCH */
/* ============================================ */
--switch: var(--background-plain);
/* ============================================ */
/* FOOTER */
/* ============================================ */
--footer: var(--background-plain);
--footer-line: var(--border);
--footer-inverse: var(--inverse);
/* ============================================ */
/* SCROLLBAR */
/* ============================================ */
--scrollbar-track: var(--background-1);
--scrollbar-thumb: var(--surface-2);
--scrollbar-track-inverse: transparent;
--scrollbar-thumb-inverse: var(--foreground-inverse);` +
generateChartTokensLight(name) +
generateMapTokens(name);
}
function generateDarkModeTokens(name, tailwindGray, useCustomGray = false, hue = 0, style = 'vibrant') {
const g = useCustomGray ? `--color-${name}-gray` : `--color-${tailwindGray}`;
const darkModePrimaryProfile = getDarkModePrimaryProfile(name, hue, style, g);
const switchColor = darkModePrimaryProfile.switchValue;
const primaryForeground = darkModePrimaryProfile.foregroundValue;
return `
/* ============================================ */
/* DARK MODE - ${useCustomGray ? 'Custom Gray' : 'Tailwind ' + tailwindGray} */
/* ============================================ */
/* BACKGROUNDS */
--background: var(${g}-800);
--background-1: var(${g}-900);
--background-2: var(${g}-950);
--background-plain: var(${g}-800);
/* TEXT */
--foreground: var(${g}-200);
--foreground-inverse: var(--color-white);
--inverse: var(${g}-950);
/* BORDERS */
--border: var(${g}-700);
--border-line-inverse: var(${g}-200);
--border-line-1: var(${g}-800);
--border-line-2: var(${g}-700);
--border-line-3: var(${g}-600);
--border-line-4: var(${g}-500);
--border-line-5: var(${g}-400);
--border-line-6: var(${g}-300);
--border-line-7: var(${g}-200);
--border-line-8: var(${g}-100);
/* PRIMARY STATES */
--primary: var(--color-primary-${darkModePrimaryProfile.primaryShade});
--primary-line: transparent;
--primary-foreground: ${primaryForeground};
--primary-hover: var(--color-primary-${darkModePrimaryProfile.interactionShade});
--primary-focus: var(--color-primary-${darkModePrimaryProfile.interactionShade});
--primary-active: var(--color-primary-${darkModePrimaryProfile.interactionShade});
--primary-checked: var(--color-primary-${darkModePrimaryProfile.checkedShade});
/* SECONDARY */
--secondary: var(--color-white);
--secondary-line: transparent;
--secondary-foreground: var(${g}-800);
--secondary-hover: var(${g}-100);
--secondary-focus: var(${g}-100);
--secondary-active: var(${g}-100);
/* LAYER */
--layer: var(--background);
--layer-line: var(--border);
--layer-foreground: var(--color-white);
--layer-hover: var(${g}-700);
--layer-focus: var(${g}-700);
--layer-active: var(${g}-700);
/* SURFACE */
--surface: var(${g}-700);
--surface-1: var(${g}-600);
--surface-2: var(${g}-500);
--surface-3: var(${g}-600);
--surface-4: var(${g}-500);
--surface-5: var(${g}-400);
--surface-line: transparent;
--surface-foreground: var(${g}-200);
--surface-hover: var(${g}-600);
--surface-focus: var(${g}-600);
--surface-active: var(${g}-600);
/* MUTED */
--muted: var(${g}-800);
--muted-foreground: var(${g}-500);
--muted-foreground-1: var(${g}-400);
--muted-foreground-2: var(${g}-300);
--muted-hover: var(${g}-700);
--muted-focus: var(${g}-700);
--muted-active: var(${g}-700);
/* DESTRUCTIVE */
--destructive: var(--color-red-500);
--destructive-foreground: var(--color-white);
--destructive-hover: var(--color-red-600);
--destructive-focus: var(--color-red-600);
/* NAVBAR */
--navbar: var(--background);
--navbar-line: var(--border);
--navbar-divider: var(--border);
--navbar-nav-foreground: var(--foreground);
--navbar-nav-hover: var(--muted-hover);
--navbar-nav-focus: var(--muted-hover);
--navbar-nav-active: var(--muted-hover);
--navbar-nav-list-divider: var(--border);
--navbar-inverse: var(--inverse);
--navbar-1: var(--background-1);
--navbar-1-line: var(--border);
--navbar-1-divider: var(--border);
--navbar-1-nav-foreground: var(--foreground);
--navbar-1-nav-hover: var(--surface);
--navbar-1-nav-focus: var(--surface);
--navbar-1-nav-active: var(--surface);
--navbar-1-nav-list-divider: var(--border);
--navbar-2: var(--background-2);
--navbar-2-line: transparent;
--navbar-2-divider: var(--border);
--navbar-2-nav-foreground: var(--foreground);
--navbar-2-nav-hover: var(--surface);
--navbar-2-nav-focus: var(--surface);
--navbar-2-nav-active: var(--surface);
--navbar-2-nav-list-divider: var(--border);
/* SIDEBAR */
--sidebar: var(--background);
--sidebar-line: var(--border);
--sidebar-divider: var(--border);
--sidebar-nav-foreground: var(--foreground);
--sidebar-nav-hover: var(--muted-hover);
--sidebar-nav-focus: var(--muted-hover);
--sidebar-nav-active: var(--muted-hover);
--sidebar-nav-list-divider: var(--border);
--sidebar-inverse: var(--inverse);
--sidebar-1: var(--background-1);
--sidebar-1-line: var(--border);
--sidebar-1-divider: var(--border);
--sidebar-1-nav-foreground: var(--foreground);
--sidebar-1-nav-hover: var(--surface);
--sidebar-1-nav-focus: var(--surface);
--sidebar-1-nav-active: var(--surface);
--sidebar-1-nav-list-divider: var(--border);
--sidebar-2: var(--background-2);
--sidebar-2-line: transparent;
--sidebar-2-divider: var(--border);
--sidebar-2-nav-foreground: var(--foreground);
--sidebar-2-nav-hover: var(--surface);
--sidebar-2-nav-focus: var(--surface);
--sidebar-2-nav-active: var(--surface);
--sidebar-2-nav-list-divider: var(--border);
/* CARD */
--card: var(--background);
--card-line: var(--border);
--card-divider: var(--border);
--card-header: var(--surface);
--card-footer: var(--surface);
--card-inverse: var(--inverse);
/* DROPDOWN */
--dropdown: var(--background-1);
--dropdown-1: var(--background-2);
--dropdown-line: transparent;
--dropdown-divider: var(--border);
--dropdown-header: var(--surface);
--dropdown-footer: var(--surface);
--dropdown-item-foreground: var(--foreground);
--dropdown-item-hover: var(--muted-hover);
--dropdown-item-focus: var(--muted-hover);
--dropdown-item-active: var(--muted-hover);
--dropdown-inverse: var(--inverse);
/* SELECT */
--select: var(--background-1);
--select-1: var(--background-2);
--select-line: transparent;
--select-item-foreground: var(--foreground);
--select-item-hover: var(--muted-hover);
--select-item-focus: var(--muted-hover);
--select-item-active: var(--muted-hover);
--select-inverse: var(--inverse);
/* OVERLAY */
--overlay: var(--background);
--overlay-line: transparent;
--overlay-divider: var(--border);
--overlay-header: var(--surface);
--overlay-footer: var(--surface);
--overlay-inverse: var(--inverse);
/* POPOVER */
--popover: var(--background-1);
--popover-line: var(--border);
/* TOOLTIP */
--tooltip: var(--color-white);
--tooltip-foreground: var(${g}-800);
--tooltip-line: transparent;
/* TABLE */
--table-line: var(--border);
/* SWITCH */
--switch: ${switchColor};
/* FOOTER */
--footer: var(--background);
--footer-line: var(--border);
--footer-inverse: var(--inverse);
/* SCROLLBAR */
--scrollbar-track: var(--surface);
--scrollbar-thumb: var(--surface-3);
--scrollbar-track-inverse: var(--surface-4);
--scrollbar-thumb-inverse: var(--surface-2);` +
generateChartTokensDark(name, g, darkModePrimaryProfile) +
`
/* MAPS - dark mode adjustments */
--map-colors-default: var(--surface);
--map-colors-default-inverse: var(--surface-2);
--map-colors-border: var(--border-line-3);
--map-colors-border-inverse: var(--border-line-4);`;
}
// ============================================
// FONT SUPPORT
// ============================================
function generateFontTokens(config) {
const fonts = [];
if (config.fontSans) {
fonts.push(` /* Typography */`);
fonts.push(` --font-sans: ${config.fontSans};`);
}
if (config.fontSerif) {
fonts.push(` --font-serif: ${config.fontSerif};`);
}
if (config.fontMono) {
fonts.push(` --font-mono: ${config.fontMono};`);
}
if (fonts.length > 0) {
return '\n' + fonts.join('\n') + '\n';
}
return '';
}
// ============================================
// MAIN GENERATOR
// ============================================
/**
* Generate a complete theme CSS file from config
*
* @param {Object} config - Theme configuration
* @param {string} config.name - Theme name (kebab-case)
* @param {number} [config.hue] - Brand color hue (0-360) - required if no primaryColor
* @param {string} [config.primaryColor] - Brand color as hex (e.g., "#2F6BFF") - converts to hue
* @param {string} [config.style='vibrant'] - 'vibrant' or 'soft'
* @param {boolean} [config.useCustomDarkGray=false] - Use custom gray for dark mode
* @param {string} [config.tailwindGray='neutral'] - Tailwind gray for dark mode
* @param {string} [config.fontSans] - Optional custom sans-serif font stack
* @param {string} [config.fontSerif] - Optional custom serif font stack
* @param {string} [config.fontMono] - Optional custom monospace font stack
* @returns {string} Complete CSS theme file
*/
function generateTheme(config) {
// Validate config
const errors = validateConfig(config);
if (errors.length > 0) {
throw new Error('Invalid config:\n - ' + errors.join('\n - '));
}
// Resolve hue from primaryColor if provided
let hue = config.hue;
if (config.primaryColor && typeof hue !== 'number') {
hue = hexToHue(config.primaryColor);
}
const {
name,
style = 'vibrant',
useCustomDarkGray = false,
tailwindGray = 'neutral',
} = config;
const themeName = `theme-${name}`;
const displayName = name.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ');
// Choose Tailwind gray based on warmth if not specified
const darkGray = tailwindGray || (hue > 30 && hue < 200 ? 'stone' : 'neutral');
const brandPalette = generateBrandPalette(name, hue, style);
const grayPalette = generateGrayPalette(name, hue);
const fontTokens = generateFontTokens(config);
const lightTokens = generateLightModeTokens(name, hue, style);
const darkTokens = generateDarkModeTokens(name, darkGray, useCustomDarkGray, hue, style);
return `/* ------------------------------ */
/* ---------- ${displayName} ----------- */
/* ------------------------------ */
@theme ${themeName} inline {
/* Brand palette (${style}) */
${brandPalette}
/* Gray palette */
${grayPalette}
}
:root[data-theme="${themeName}"],
[data-theme="${themeName}"] {
${fontTokens}${lightTokens}
}
[data-theme="${themeName}"].dark {
${darkTokens}
}
`;
}
module.exports = {
generateTheme,
generateBrandPalette,
generateGrayPalette,
oklchToHex,
hexToHue,
validateConfig
};
Related skills
How it compares
Pick preline-theme-generator over hand-written CSS when you need Preline-compliant semantic tokens and a single validated theme file per brand.
FAQ
What file does preline-theme-generator create?
preline-theme-generator creates exactly one standalone <name>.css theme file per task. The file activates with data-theme="theme-<name>" and optional .dark overrides without editing shared theme.css or other project files.
Can preline-theme-generator use npx or remote packages?
preline-theme-generator forbids npx and network-fetched packages. Generation must go through the bundled scripts find-themes-dir.js and run-theme-generator.js after normalizing hue, primaryColor, and style inputs.
Is Preline Theme Generator safe to install?
skills.sh reports 2 of 3 security scanners passed. Review the Security Audits panel on this page before installing in production.