TypeScript type generation and runtime resolution for Tailwind CSS v4 theme variables.
Transform your Tailwind v4 CSS variables into fully-typed TypeScript interfaces and runtime objects with automatic conflict detection, intelligent nesting, and comprehensive diagnostics.
- Quick Start
- Core Concepts
- Installation
- Usage
- Configuration
- Advanced Features
- Examples
- Debugging
- Requirements
- Contributing
- License
1. Install:
npm install -D tailwind-resolver2. Configure Vite plugin:
// vite.config.ts import tailwindcss from '@tailwindcss/vite'; import { tailwindResolver } from 'tailwind-resolver/vite'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [ tailwindcss(), tailwindResolver({ input: 'src/styles.css', // Your Tailwind CSS file }), ], });3. Use the generated theme:
import { tailwind } from './generated/tailwindcss'; // Fully typed with autocomplete const primaryColor = tailwind.variants.default.colors.primary[500]; const darkBackground = tailwind.variants.dark.colors.background;That's it! The plugin automatically generates TypeScript types and runtime objects from your Tailwind CSS variables.
Tailwind Theme Resolver parses your Tailwind CSS v4 files and generates:
- TypeScript Types - Full type safety with autocomplete for all theme properties
- Runtime Objects - JavaScript objects mirroring your CSS variables for use in Canvas, Chart.js, etc.
- Diagnostic Reports - Automatic detection of CSS conflicts and unresolved variables
- Theme Variants - Support for dark mode, custom themes, and CSS selector-based variants
All Tailwind CSS v4 namespaces are supported:
{ colors: {}, // --color-* spacing: {}, // --spacing-* (with dynamic calc helper) fonts: {}, // --font-* fontSize: {}, // --text-* fontWeight: {}, // --font-weight-* tracking: {}, // --tracking-* leading: {}, // --leading-* breakpoints: {}, // --breakpoint-* containers: {}, // --container-* radius: {}, // --radius-* shadows: {}, // --shadow-* insetShadows: {}, // --inset-shadow-* dropShadows: {}, // --drop-shadow-* textShadows: {}, // --text-shadow-* blur: {}, // --blur-* perspective: {}, // --perspective-* aspect: {}, // --aspect-* ease: {}, // --ease-* animations: {}, // --animate-* defaults: {}, // --default-* keyframes: {} // @keyframes }With default configuration, the plugin generates:
src/generated/tailwindcss/ ├── types.ts # TypeScript interfaces ├── theme.ts # Runtime theme objects ├── index.ts # Convenient re-exports ├── conflicts.md # CSS conflict report (if conflicts detected) ├── conflicts.json # Machine-readable conflict data ├── unresolved.md # Unresolved variable report (if any detected) └── unresolved.json # Machine-readable unresolved data # Bun bun add -D tailwind-resolver # pnpm pnpm add -D tailwind-resolver # Yarn yarn add -D tailwind-resolver # npm npm install -D tailwind-resolverRecommended for most projects. Generates types once during build, with hot-reload during development.
Basic Configuration:
import tailwindcss from '@tailwindcss/vite'; import { tailwindResolver } from 'tailwind-resolver/vite'; import { defineConfig } from 'vite'; export default defineConfig({ plugins: [ tailwindcss(), tailwindResolver({ input: 'src/styles.css', }), ], });Full Configuration:
tailwindResolver({ // Required: Path to CSS file (relative to Vite project root) input: 'src/styles.css', // Optional: Output directory (default: auto-detected) outputDir: 'src/generated/tailwindcss', // Optional: Resolve @import statements (default: true) resolveImports: true, // Optional: Runtime generation control (default: true) generateRuntime: { variants: true, // Include theme variants selectors: true, // Include CSS selectors files: false, // Exclude file list (production) variables: false, // Exclude raw variables (production) reports: { conflicts: true, // Generate conflict reports unresolved: true, // Generate unresolved variable reports }, }, // Optional: Control Tailwind defaults (default: true) includeDefaults: true, // Optional: Nesting configuration nesting: { colors: { maxDepth: 2 }, default: { maxDepth: 3 }, }, // Optional: Theme overrides overrides: { '*': { 'fonts.sans': 'Inter, sans-serif' }, dark: { 'colors.background': '#000000' }, }, // Optional: Debug logging (default: false) debug: false, });Usage in Code:
import { dark, defaultTheme, tailwind } from './generated/tailwindcss'; // Use the master tailwind object const primary = tailwind.variants.default.colors.primary[500]; const darkBg = tailwind.variants.dark.colors.background; // Or use individual variant exports const primary2 = defaultTheme.colors.primary[500]; const darkBg2 = dark.colors.background;For dynamic scenarios like CLI tools, server-side rendering, or runtime theme switching.
Type-Safe Usage (Recommended):
import type { Tailwind } from './generated/tailwindcss'; import { resolveTheme } from 'tailwind-resolver'; const result = await resolveTheme<Tailwind>({ input: './src/styles.css', }); // Fully typed with autocomplete result.variants.default.colors.primary[500]; result.variants.dark.colors.background; result.selectors.dark; // '[data-theme="dark"]'Note: Generate types using the Vite plugin or CLI before using the runtime API for type safety.
Basic Usage:
import { resolveTheme } from 'tailwind-resolver'; const result = await resolveTheme({ input: './src/styles.css', }); console.log(result.variants.default.colors.primary[500]); console.log(result.variants.dark.colors.background);Full Configuration:
const result = await resolveTheme({ // Option 1: CSS file path input: './src/styles.css', // Option 2: Raw CSS content // css: '@theme { --color-primary: blue; }', // Optional: Base path for @import resolution basePath: process.cwd(), // Optional: Resolve @import statements (default: true) resolveImports: true, // Optional: Control Tailwind defaults (default: true) includeDefaults: true, // Optional: Nesting configuration nesting: { colors: { maxDepth: 2 }, default: { maxDepth: 3 }, }, // Optional: Theme overrides overrides: { default: { 'fonts.sans': 'Inter' }, }, // Optional: Debug logging (default: false) debug: false, });Generate types without a build tool:
Basic Usage:
bunx tailwind-resolver -i src/styles.cssCommon Options:
# Specify output directory bunx tailwind-resolver -i src/styles.css -o src/generated # Types only (no runtime objects) bunx tailwind-resolver -i src/styles.css --no-runtime # Include only specific Tailwind defaults bunx tailwind-resolver -i src/styles.css --include-defaults colors,spacing # Exclude specific Tailwind defaults bunx tailwind-resolver -i src/styles.css --exclude-defaults shadows,animations # Generate only conflict reports bunx tailwind-resolver -i src/styles.css --reports conflicts # Debug mode bunx tailwind-resolver -i src/styles.css --debugAll Options:
-i, --input <path> CSS input file (required) -o, --output <path> Output directory (default: auto-detected) -r, --runtime Generate runtime objects (default: true) --no-runtime Types only --include-defaults [categories] Include only specified Tailwind defaults (comma-separated) --exclude-defaults [categories] Exclude specified Tailwind defaults (comma-separated) --reports [categories] Generate only specified reports (conflicts, unresolved) --exclude-reports [categories] Exclude specified reports (comma-separated) -d, --debug Enable debug mode -h, --help Show help Control which Tailwind CSS default theme values are included.
Include All Defaults (Default):
includeDefaults: true;Exclude All Defaults:
includeDefaults: false;Selective Inclusion:
includeDefaults: { colors: true, // Include default colors spacing: true, // Include default spacing fonts: true, // Include default fonts fontSize: true, // Include default font sizes fontWeight: true, // Include default font weights tracking: false, // Exclude tracking leading: false, // Exclude leading shadows: false, // Exclude shadows animations: false, // Exclude animations // ... 21 categories total }Disable Specific Defaults via CSS:
Use initial keyword in @theme blocks (Tailwind v4 docs):
@theme { /* Remove specific values */ --color-lime-*: initial; --spacing-4: initial; /* Remove entire categories */ --color-*: initial; --spacing-*: initial; /* Custom values are preserved */ --color-primary-500: #3b82f6; }Combining Approaches:
// Configuration: Include colors and spacing includeDefaults: { colors: true, spacing: true, shadows: false, } // CSS: Remove specific colors @theme { --color-lime-*: initial; // Excludes lime from included colors --color-fuchsia-*: initial; // Excludes fuchsia from included colors } // Result: All default colors EXCEPT lime and fuchsiaPriority: CSS initial declarations take precedence over includeDefaults configuration.
Control how CSS variable names are parsed into nested theme structures.
Default Behavior:
Without configuration, all dashes create nesting levels:
--color-tooltip-outline-50: #fff; /* → colors.tooltip.outline[50] */Limit Nesting Depth:
nesting: { colors: { maxDepth: 2 }, // --color-brand-primary-hover-500 // → colors.brand.primaryHover500 (2 levels, rest flattened to camelCase) }Flatten Mode:
Control how parts beyond maxDepth are flattened:
nesting: { colors: { maxDepth: 2, flattenMode: 'camelcase', // Default: blueSkyLight50 // flattenMode: 'literal', // Alternative: 'blue-sky-light-50' } }Consecutive Dashes Handling:
nesting: { colors: { consecutiveDashes: 'exclude', // Default: skip variables with -- // consecutiveDashes: 'nest', // Treat -- as single - // consecutiveDashes: 'camelcase',// Convert to camelCase // consecutiveDashes: 'literal', // Preserve dash } }Per-Namespace Configuration:
nesting: { default: { maxDepth: 1 }, // Apply to all namespaces colors: { maxDepth: 3 }, // Override for colors shadows: { maxDepth: 2 }, // Override for shadows radius: { maxDepth: 0 }, // Completely flat }Complete Example:
nesting: { colors: { maxDepth: 2, flattenMode: 'literal', consecutiveDashes: 'camelcase', } } // CSS: --color-tooltip--outline-hover-50 // Step 1: tooltipOutline (consecutive dashes → camelCase) // Step 2: maxDepth: 2 → colors.tooltipOutline.hover['50']See Nesting Configuration Details for comprehensive documentation.
Apply programmatic overrides to theme values without modifying CSS files.
Use Cases:
- Inject external variables (Next.js fonts, plugin variables)
- Fix variant-specific values
- Global customization across all variants
- Quick prototyping
Flat Notation:
overrides: { default: { 'colors.primary.500': '#custom-blue', 'radius.lg': '0.5rem', }, dark: { 'colors.background': '#000000', }, '*': { // Wildcard - applies to all variants 'fonts.sans': 'Inter, sans-serif', } }Nested Notation:
overrides: { default: { colors: { primary: { 500: '#custom-blue' } }, radius: { lg: '0.5rem' } } }Detailed Control:
overrides: { dark: { 'radius.lg': { value: '0', force: true, // Apply even for low-confidence conflicts resolveVars: false // Skip variable resolution } } }Selector Matching:
overrides: { 'dark': {}, // Variant name (preferred) '[data-theme="dark"]': {}, // CSS selector (verbose) 'default': {}, // Default theme 'base': {}, // Alias for 'default' '*': {}, // All variants 'themeInter': {}, // .theme-inter → themeInter (camelCase) }Note: Multi-word variant names are automatically converted to camelCase:
- CSS:
.theme-noto-sans→ Override key:'themeNotoSans'
See Theme Overrides Details for comprehensive documentation.
Control diagnostic report generation.
Enable All Reports (Default):
generateRuntime: { reports: true, }Disable All Reports:
generateRuntime: { reports: false, }Granular Control:
generateRuntime: { reports: { conflicts: true, // CSS conflict reports unresolved: false, // Unresolved variable reports } }CLI:
# Disable all reports bunx tailwind-resolver -i src/styles.css --no-reports # Generate only conflict reports bunx tailwind-resolver -i src/styles.css --reports conflicts # Exclude unresolved variable reports bunx tailwind-resolver -i src/styles.css --exclude-reports unresolvedAutomatically detects when CSS rules override CSS variables and ensures runtime theme matches actual rendered styles.
Problem:
.theme-mono { --radius-lg: 0.45em; /* CSS variable */ .rounded-lg { border-radius: 0; /* CSS rule - overrides the variable! */ } }Solution:
The resolver:
- Detects all conflicts between CSS rules and variables
- Applies high-confidence overrides automatically
- Reports complex cases for manual review
Confidence Levels:
- High (auto-applied): Static values, simple selectors
- Medium/Low (manual review): Dynamic values, pseudo-classes, media queries, complex selectors
Generated Reports:
src/generated/tailwindcss/ ├── conflicts.md # Human-readable report with recommendations └── conflicts.json # Machine-readable for CI/CD Terminal Output:
✓ Theme types generated successfully Generated files: - src/generated/tailwindcss/types.ts - src/generated/tailwindcss/theme.ts - src/generated/tailwindcss/index.ts ⚠ 12 CSS conflicts detected (see src/generated/tailwindcss/conflicts.md) Detects CSS variables with var() references that couldn't be resolved.
Problem:
@theme { --font-sans: var(--font-inter); /* Injected by Next.js */ --color-accent: var(--tw-primary); /* Tailwind plugin variable */ }Solution:
The resolver categorizes unresolved variables:
- Unknown - May need definition or verification
- External - From plugins, frameworks, or external stylesheets
- Self-referential - Intentionally left unresolved
Generated Reports:
src/generated/tailwindcss/ ├── unresolved.md # Human-readable with actionable recommendations └── unresolved.json # Machine-readable for CI/CD Terminal Output:
ℹ 8 unresolved variables detected (see src/generated/tailwindcss/unresolved.md) The spacing property is both an object AND a callable function for dynamic calculations.
Static Values:
defaultTheme.spacing.xs; // '0.75rem' defaultTheme.spacing.base; // '0.25rem'Dynamic Calculations:
defaultTheme.spacing(4); // 'calc(0.25rem * 4)' → 1rem defaultTheme.spacing(16); // 'calc(0.25rem * 16)' → 4rem defaultTheme.spacing(-2); // 'calc(0.25rem * -2)' → -0.5remUsage:
<div style={{ padding: defaultTheme.spacing(4), // Same as Tailwind's p-4 margin: defaultTheme.spacing(-2), // Same as Tailwind's -m-2 width: defaultTheme.spacing(64), // Same as Tailwind's w-64 }} />Why: Tailwind generates utilities like p-4, m-8, w-16 using calc(var(--spacing) * N). This helper replicates that behavior for runtime use.
Tailwind Utilities Using Spacing:
- Layout:
inset-<n>,m-<n>,p-<n>,gap-<n> - Sizing:
w-<n>,h-<n>,min-w-<n>,max-w-<n> - Typography:
indent-<n>,border-spacing-<n>,scroll-m-<n>
Note: Requires --spacing-base in your CSS theme.
Full TypeScript type safety with the generated Tailwind interface.
Generated Constant:
import { tailwind } from './generated/tailwindcss'; // Fully typed with autocomplete tailwind.variants.default.colors.primary[500]; // ✓ tailwind.variants.dark.colors.background; // ✓ tailwind.selectors.dark; // ✓Runtime API:
import type { Tailwind } from './generated/tailwindcss'; import { resolveTheme } from 'tailwind-resolver'; const result = await resolveTheme<Tailwind>({ input: './theme.css', }); // Same structure, same types result.variants.default.colors.primary[500]; // ✓ result.variants.dark.colors.background; // ✓ result.selectors.dark; // ✓Autocomplete: Works automatically when output directory is in tsconfig.json includes.
import { tailwind } from './generated/tailwindcss'; new Chart(ctx, { data: { datasets: [ { backgroundColor: [ tailwind.variants.default.colors.primary[500], tailwind.variants.dark.colors.secondary[500], ], }, ], }, });import { defaultTheme } from './generated/tailwindcss'; ctx.fillStyle = defaultTheme.colors.background; ctx.font = `${defaultTheme.fontSize.xl.size} ${defaultTheme.fonts.display}`;import { tailwind } from './generated/tailwindcss'; const currentTheme = isDark ? tailwind.variants.dark : tailwind.variants.default; chartInstance.data.datasets[0].backgroundColor = currentTheme.colors.primary[500]; chartInstance.update();@theme { --color-background: #ffffff; } [data-theme='dark'] { --color-background: #1f2937; }import { dark, defaultTheme, selectors } from './generated/tailwindcss'; console.log(defaultTheme.colors.background); // '#ffffff' console.log(dark.colors.background); // '#1f2937' console.log(selectors.dark); // "[data-theme='dark']"Enable debug mode to see detailed logging.
Vite:
tailwindResolver({ input: 'src/styles.css', debug: true });CLI:
bunx tailwind-resolver -i src/styles.css --debugRuntime API:
resolveTheme({ input: './theme.css', debug: true });Output:
[Tailwind Theme Resolver] Failed to resolve import: ./components/theme.css Resolved path: /Users/you/project/src/components/theme.css Error: ENOENT: no such file or directory [Overrides] Injected variable: --radius-lg = 0.5rem [Overrides] Applied to 'default': radius.lg = 0.5rem - Node.js >= 18 or Bun >= 1.0
- TypeScript >= 5.0 (for type generation)
- Vite >= 5.0 (for Vite plugin only)
Issues and pull requests welcome on GitHub.
MIT
Complete documentation for nesting configuration options.
Without configuration, every dash creates a nesting level:
@theme { --color-tooltip-outline-50: #fff; /* → colors.tooltip.outline[50] */ --shadow-elevation-high-focus: 0 0 0; /* → shadows.elevation.high.focus */ }Limit nesting levels. After the limit, remaining parts are flattened.
Configuration:
nesting: { colors: { maxDepth: 2; } }Results:
--color-tooltip-outline-50: #fff; /* Before: colors.tooltip.outline[50] */ /* After: colors.tooltip.outline['50'] (no change - only 3 parts) */ --color-brand-primary-hover-500: #3b82f6; /* Before: colors.brand.primary.hover[500] */ /* After: colors.brand.primaryHover500 (2 levels, rest flattened) */Special Case - maxDepth: 0:
nesting: { default: { maxDepth: 0 } } // --color-brand-primary-dark: #000 // → colors.brandPrimaryDark (completely flat)Control how consecutive dashes (--) are processed:
Options:
'exclude'(default, matches Tailwind v4) - Skip variables with--'nest'- Treat--as single-'camelcase'- Convert--to camelCase boundary'literal'- Preserve--in keys
Configuration:
nesting: { colors: { consecutiveDashes: 'camelcase'; } }Results:
--color-button--primary: #fff; /* 'exclude' (default): Not included */ /* 'nest': colors.button.primary */ /* 'camelcase': colors.buttonPrimary */ /* 'literal': colors['button-'].primary */Control how parts beyond maxDepth are flattened:
Options:
'camelcase'(default) - Flatten to camelCase'literal'- Flatten to kebab-case string key
Configuration:
nesting: { colors: { maxDepth: 2, flattenMode: 'literal' } }Results:
--color-blue-sky-light-50: #e0f2fe; /* flattenMode: 'camelcase' (default) */ /* → colors.blue.skyLight50 */ /* flattenMode: 'literal' */ /* → colors.blue['sky-light-50'] */Note: Only applies when maxDepth is reached.
nesting: { colors: { maxDepth: 2, consecutiveDashes: 'camelcase', flattenMode: 'literal', } }--color-tooltip--outline-hover-50: #fff; /* Step 1: tooltipOutline (consecutive dashes → camelCase) */ /* Step 2: maxDepth: 2 → colors.tooltipOutline.hover['50'] */nesting: { colors: { maxDepth: 3 }, // Deep nesting shadows: { maxDepth: 2 }, // Moderate nesting spacing: { maxDepth: 1 }, // Flat structure radius: { consecutiveDashes: 'camelcase' }, }nesting: { default: { maxDepth: 1 }, // Apply to all namespaces colors: { maxDepth: 3 }, // Override for colors }When both scalar and nested variables exist at the same path, the scalar moves to DEFAULT:
@theme { --color-card: blue; --color-card-foreground: white; }// Result: { colors: { card: { DEFAULT: 'blue', foreground: 'white' } } }Works in Any Order:
/* Nested first, then scalar */ --color-card-foreground: white; --color-card: blue; /* Same result: { card: { DEFAULT: 'blue', foreground: 'white' } } */With Color Scales:
--color-blue-500: #3b82f6; --color-blue-600: #2563eb; --color-blue: #1d4ed8;// Result: { blue: { DEFAULT: '#1d4ed8', 500: '#3b82f6', 600: '#2563eb' } }1. Consistent Flat Structure:
nesting: { default: { maxDepth: 0 } } // All variables flattened: // --color-brand-primary-500 → colors.brandPrimary5002. BEM-Style Naming:
nesting: { default: { maxDepth: 1, consecutiveDashes: 'camelcase' } } // --color-button--primary → colors.buttonPrimary // --color-input--error → colors.inputError3. Controlled Depth by Category:
nesting: { colors: { maxDepth: 3 }, // Deep color scales shadows: { maxDepth: 2 }, // Moderate shadow variants radius: { maxDepth: 1 }, // Flat radius values }# Limit nesting depth globally bunx tailwind-resolver -i src/styles.css --nesting-max-depth 2 # Control consecutive dashes bunx tailwind-resolver -i src/styles.css --nesting-consecutive-dashes camelcase # Control flatten mode bunx tailwind-resolver -i src/styles.css --nesting-flatten-mode literal # Combine options bunx tailwind-resolver -i src/styles.css \ --nesting-max-depth 2 \ --nesting-consecutive-dashes nest \ --nesting-flatten-mode literalNote: CLI flags apply globally to all namespaces. For per-namespace control, use Vite plugin or Runtime API.
Complete documentation for theme overrides.
- Inject external variables - Provide values for Next.js fonts, Tailwind plugins, or external sources
- Fix variant-specific values - Override dark mode or custom theme properties
- Global customization - Apply consistent values across all variants
- Quick prototyping - Test theme changes without editing CSS
Flat Notation (Dot-Separated Paths):
overrides: { default: { 'colors.primary.500': '#custom-blue', 'radius.lg': '0.5rem', 'fonts.sans': 'Inter, sans-serif' } }Nested Notation:
overrides: { default: { colors: { primary: { 500: '#custom-blue' } }, radius: { lg: '0.5rem' } } }Mix and Match:
overrides: { default: { 'colors.primary.500': '#custom-blue', radius: { lg: '0.5rem' } } }Variant Names (Recommended):
overrides: { 'dark': { 'colors.background': '#000' }, 'themeInter': { 'fonts.sans': 'Inter' }, // .theme-inter → themeInter }CSS Selectors (Verbose):
overrides: { '[data-theme="dark"]': { 'colors.background': '#000' }, }Special Keys:
overrides: { 'default': {}, // Default theme 'base': {}, // Alias for 'default' '*': {}, // All variants (wildcard) }Important: Variant names are automatically converted from kebab-case to camelCase:
- CSS:
.theme-inter→ Override key:'themeInter' - CSS:
.theme-noto-sans→ Override key:'themeNotoSans'
overrides: { dark: { 'radius.lg': { value: '0', force: true, // Apply even for low-confidence conflicts resolveVars: false // Skip variable resolution (post-resolution only) } } }1. Injecting External Variables:
overrides: { default: { 'fonts.sans': 'var(--font-inter)', // Next.js font 'colors.primary': 'var(--tw-primary)' // Tailwind plugin } }2. Variant-Specific Overrides:
overrides: { dark: { 'colors.background': '#000000', 'colors.foreground': '#ffffff' }, compact: { 'radius.lg': '0', 'spacing.base': '0.125rem' } }3. Global Overrides:
overrides: { '*': { 'fonts.sans': 'Inter, -apple-system, BlinkMacSystemFont, sans-serif', 'fonts.mono': 'JetBrains Mono, Consolas, monospace' } }4. Prototyping:
overrides: { default: { 'colors.primary.500': '#ff6b6b', 'radius.lg': '1rem' } }Two-phase approach:
-
Pre-resolution (Variable Injection)
- Injects synthetic CSS variables before resolution
- Allows overrides to participate in
var()resolution - Applied to:
'default','base','*'selectors
-
Post-resolution (Theme Mutation)
- Directly mutates resolved theme objects
- Overrides final computed values
- Applied to: all selector types
tailwindResolver({ input: 'src/styles.css', debug: true, overrides: { default: { 'radius.lg': '0.5rem' }, }, });Output:
[Overrides] Injected variable: --radius-lg = 0.5rem [Overrides] Injected 1 variables for 'default' [Overrides] Applied to 'default': radius.lg = 0.5rem [Overrides] Summary for 'default': 1 applied, 0 skipped Ensure the output directory is included in tsconfig.json:
{ "include": ["src/**/*"], "compilerOptions": { "skipLibCheck": true } }If you find this helpful, follow me on X @mrstern_