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:

// 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

TokenCSS variableTailwind class
Primary color--salt-color-primarybg-salt-primary, text-salt-primary
On-primary--salt-color-on-primarytext-salt-on-primary
Muted text--salt-color-mutedtext-salt-muted
Surface--salt-color-surfacebg-salt-surface
Border--salt-color-borderborder-salt-border
Danger--salt-color-dangerbg-salt-danger, text-salt-danger
Palette step--salt-palette-danger-100bg-salt-palette-danger-100
Surface card--salt-surface-cardbg-salt-surface-card
State hover--salt-state-primary-hoverhover:bg-salt-state-primary-hover
Medium spacing--salt-spacing-mdp-salt-md, px-salt-md, gap-salt-md
Large radius--salt-radius-lgrounded-salt-lg
Body font size--salt-font-size-mdtext-salt-md

Checklist

  • generateTheme() called in lib/theme.ts
  • generateCssVariables(theme, { format: 'both' }) → CSS injected into layout ✓
  • generateTailwindConfig(theme).extend spread into theme.extend in config ✓
  • darkMode: ['attribute', '[data-theme="dark"]'] set ✓
  • data-theme toggled 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 →