The pain this guide solves
Tailwind's default palette is beautiful but it's not your brand. You add custom colors to tailwind.config.js, then maintain two separate color systems — one for Tailwind utilities and one for the rest of your CSS. Dark mode doubles the work.
salt-theme-gen with Tailwind CSS
What you will build
- Tailwind utilities driven by salt-theme-gen tokens:
bg-salt-primary,text-salt-muted,border-salt-border,rounded-salt-md,p-salt-lg - A single source of truth — change the preset, both Tailwind classes and CSS variables update
- Dark mode via CSS custom properties — no
dark:duplication needed for most cases - All 23 semantic colors, 88 tonal palette steps, 32 state colors, 4 surface elevations, spacing, radius, and font sizes available as utilities
Time required: 10 minutes.
Install
npm install salt-theme-gen
npm install -D tailwindcss
Step 1 — Generate the Tailwind config
generateTailwindConfig() returns a ready-to-spread theme.extend object. Colors are CSS var() references so dark mode switching happens automatically when the CSS variables change.
// lib/theme.ts
import { generateTheme, generateTailwindConfig, generateCssVariables } from 'salt-theme-gen';
export const theme = generateTheme({
preset: 'ocean',
spacing: 'default',
radius: 'default',
fontSize: 'default',
});
// theme.extend — spread into tailwind.config.ts
// css — inject into your stylesheet or layout
export const { extend } = generateTailwindConfig(theme);
export const { css: themeCss } = generateCssVariables(theme, { format: 'both' });
Step 2 — Wire into Tailwind v3
// tailwind.config.ts
import type { Config } from 'tailwindcss';
import { extend } from './lib/theme';
const config: Config = {
content: ['./src/**/*.{html,js,ts,jsx,tsx,vue,svelte,astro}'],
darkMode: ['attribute', '[data-theme="dark"]'],
theme: { extend },
plugins: [],
};
export default config;
That’s it. extend contains colors, spacing, borderRadius, and fontSize — all wired to --salt-* CSS variables.
Step 3 — Inject the CSS variables
The Tailwind config maps class names to var(--salt-*) references. You need those variables in your CSS. Pick one:
Build script (recommended for static sites)
// scripts/build-theme.mjs
import { generateTheme, generateCssVariables } from 'salt-theme-gen';
import { writeFileSync, mkdirSync } from 'node:fs';
const theme = generateTheme({ preset: 'ocean' });
const { css } = generateCssVariables(theme, { format: 'both' });
mkdirSync('public', { recursive: true });
writeFileSync('public/theme.css', css, 'utf8');
<!-- index.html -->
<link rel="stylesheet" href="/theme.css" />
Runtime inject (SPAs)
// src/main.ts
import { themeCss } from './lib/theme';
const style = document.createElement('style');
style.textContent = themeCss;
document.head.prepend(style);
In your main CSS file
/* src/styles/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
/* Your base styles can now use the generated variables */
body {
background-color: var(--salt-color-background);
color: var(--salt-color-text);
font-size: var(--salt-font-size-md);
}
Available utility classes
All tokens use the salt- prefix in Tailwind class names:
Semantic colors
<!-- Brand colors -->
<div class="bg-salt-primary text-salt-on-primary">Primary</div>
<div class="bg-salt-secondary text-salt-on-secondary">Secondary</div>
<div class="bg-salt-danger text-salt-on-danger">Danger</div>
<div class="bg-salt-success text-salt-on-success">Success</div>
<!-- Surfaces -->
<div class="bg-salt-background text-salt-text">Page</div>
<div class="bg-salt-surface border border-salt-border">Card</div>
<div class="text-salt-muted">Caption text</div>
<!-- Surface elevations -->
<div class="bg-salt-surface-card">Card elevation</div>
<div class="bg-salt-surface-modal">Modal</div>
<div class="bg-salt-surface-popover">Dropdown</div>
State colors
<!-- Interactive states as background colors -->
<div class="hover:bg-salt-state-primary-hover">...</div>
<div class="active:bg-salt-state-primary-pressed">...</div>
<div class="focus-visible:bg-salt-state-primary-focused">...</div>
<div class="disabled:bg-salt-state-primary-disabled">...</div>
Tonal palette steps
All 88 palette steps (8 intents × 11 steps) are available:
<!-- Severity badge using palette steps -->
<span class="
bg-salt-palette-danger-50
border border-salt-palette-danger-200
text-salt-palette-danger-700
rounded-salt-pill px-salt-sm py-salt-xs text-salt-xs
">
Error
</span>
<!-- Chart bars using progressive primary steps -->
<div class="bg-salt-palette-primary-200 h-8 w-4"></div>
<div class="bg-salt-palette-primary-400 h-12 w-4"></div>
<div class="bg-salt-palette-primary-600 h-16 w-4"></div>
<div class="bg-salt-palette-primary-800 h-20 w-4"></div>
Spacing, radius, font size
<!-- Spacing: p-salt-sm, p-salt-md, gap-salt-lg, m-salt-xl ... -->
<div class="p-salt-md gap-salt-sm">
<!-- Radius: rounded-salt-sm, rounded-salt-md, rounded-salt-pill ... -->
<button class="rounded-salt-md">
<!-- Font size: text-salt-xs through text-salt-3xl -->
<h1 class="text-salt-3xl">Heading</h1>
<p class="text-salt-md">Body</p>
<small class="text-salt-xs">Caption</small>
Full button example
<!-- Primary button — all tokens, no hardcoded values -->
<button class="
bg-salt-primary text-salt-on-primary
hover:bg-salt-state-primary-hover
active:bg-salt-state-primary-pressed
focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-salt-state-primary-focused
disabled:bg-salt-state-primary-disabled disabled:cursor-not-allowed
rounded-salt-md px-salt-lg py-salt-sm
text-salt-md font-semibold
transition-colors
">
Save changes
</button>
Dark mode
Set data-theme="dark" on <html> to switch all CSS variables at once. You rarely need dark: prefix variants — the CSS variables handle it.
// Toggle dark mode
const root = document.documentElement;
root.dataset.theme = root.dataset.theme === 'dark' ? 'light' : 'dark';
// Sync with system preference
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
document.documentElement.dataset.theme = 'dark';
}
Use dark: only when you need a value that cannot be expressed as a CSS variable — shadows, opacity, or non-token overrides:
<!-- CSS variables handle color — no dark: needed -->
<div class="bg-salt-surface text-salt-text border-salt-border">
<!-- dark: only for non-token values -->
<div class="shadow-sm dark:shadow-xl bg-salt-surface">
Tailwind v4
In Tailwind v4, map tokens directly in your CSS using @theme:
/* src/app.css */
@import 'tailwindcss';
/* Import the generated CSS variables */
@import './theme.css';
/* Map --salt-* variables into Tailwind's design system */
@theme {
--color-salt-primary: var(--salt-color-primary);
--color-salt-secondary: var(--salt-color-secondary);
--color-salt-background: var(--salt-color-background);
--color-salt-surface: var(--salt-color-surface);
--color-salt-text: var(--salt-color-text);
--color-salt-muted: var(--salt-color-muted);
--color-salt-border: var(--salt-color-border);
--color-salt-danger: var(--salt-color-danger);
--color-salt-success: var(--salt-color-success);
--color-salt-warning: var(--salt-color-warning);
--color-salt-info: var(--salt-color-info);
--color-salt-on-primary: var(--salt-color-on-primary);
--color-salt-on-danger: var(--salt-color-on-danger);
--spacing-salt-xs: var(--salt-spacing-xs);
--spacing-salt-sm: var(--salt-spacing-sm);
--spacing-salt-md: var(--salt-spacing-md);
--spacing-salt-lg: var(--salt-spacing-lg);
--spacing-salt-xl: var(--salt-spacing-xl);
--spacing-salt-xxl: var(--salt-spacing-xxl);
--radius-salt-sm: var(--salt-radius-sm);
--radius-salt-md: var(--salt-radius-md);
--radius-salt-lg: var(--salt-radius-lg);
--radius-salt-pill: var(--salt-radius-pill);
--font-size-salt-xs: var(--salt-font-size-xs);
--font-size-salt-md: var(--salt-font-size-md);
--font-size-salt-xl: var(--salt-font-size-xl);
--font-size-salt-3xl: var(--salt-font-size-3xl);
}
In v4 you generate the theme.css file (from generateCssVariables) and reference it via @import. No tailwind.config.ts needed.
Exporting the config as JSON
generateTailwindConfig also returns a json field — the same extend object serialized as a JSON string. Useful for tooling that reads Tailwind configs as JSON:
const { extend, json } = generateTailwindConfig(theme);
writeFileSync('tailwind-extend.json', json, 'utf8');
Token naming reference
| Token | CSS variable | Tailwind class |
|---|---|---|
| Primary color | --salt-color-primary | bg-salt-primary, text-salt-primary |
| On-primary | --salt-color-on-primary | text-salt-on-primary |
| Muted text | --salt-color-muted | text-salt-muted |
| Surface | --salt-color-surface | bg-salt-surface |
| Border | --salt-color-border | border-salt-border |
| Danger | --salt-color-danger | bg-salt-danger, text-salt-danger |
| Palette step | --salt-palette-danger-100 | bg-salt-palette-danger-100 |
| Surface card | --salt-surface-card | bg-salt-surface-card |
| State hover | --salt-state-primary-hover | hover:bg-salt-state-primary-hover |
| Medium spacing | --salt-spacing-md | p-salt-md, px-salt-md, gap-salt-md |
| Large radius | --salt-radius-lg | rounded-salt-lg |
| Body font size | --salt-font-size-md | text-salt-md |
Checklist
generateTheme()called inlib/theme.ts✓generateCssVariables(theme, { format: 'both' })→ CSS injected into layout ✓generateTailwindConfig(theme).extendspread intotheme.extendin config ✓darkMode: ['attribute', '[data-theme="dark"]']set ✓data-themetoggled on<html>for mode switching ✓- Components use
bg-salt-primary text-salt-on-primary— no hardcoded hex ✓
Keeping Tailwind’s built-in palette: Generated tokens live in a salt- namespace so they don’t conflict with Tailwind’s default colors (blue-500, gray-100). You can use both side by side.
Live demo
Open this integration in StackBlitz — fully working, editable in your browser.
Open in StackBlitz →