DEV Community

Cover image for Introducing salt-theme-gen: Generate a Complete Design System from One Color
Hasan Sarwer
Hasan Sarwer

Posted on

Introducing salt-theme-gen: Generate a Complete Design System from One Color

You have a design system problem if any of these sound familiar:

  • Your _variables.scss has 80 hardcoded colors, half of them unused
  • Dark mode is a second file someone maintains by hand (when they remember)
  • Changing the primary color means grepping through a dozen partials and hoping nothing was hardcoded elsewhere
  • The designer asked for "slightly more rounded corners" and you spent 45 minutes finding every border-radius value

salt-theme-gen is a zero-dependency TypeScript package that generates a complete design token set from a single call.

What you get

import { generateTheme } from 'salt-theme-gen';

const theme = generateTheme({
  preset:   'ocean',    // or any hex color: '#6366f1'
  spacing:  'default',
  radius:   'default',
  fontSize: 'default',
});
Enter fullscreen mode Exit fullscreen mode

One call. That's it. What comes back:

  • 21 semantic colors โ€” primary, secondary, background, surface, text, muted, border, danger, success, warning, info, and their on-colors
  • 32 interaction states โ€” hover, pressed, focused, disabled for all 8 intents
  • 4 surface elevations โ€” base, raised, card, overlay
  • 6 spacing values โ€” xs through xxl
  • 7 border-radius values โ€” sm through pill
  • 7 font sizes โ€” xs through 3xl
  • 18 WCAG accessibility checks โ€” built-in, no extra library

All in light and dark mode. Both derived automatically.

Why OKLCH instead of hex

Most design token libraries give you hex or HSL values. salt-theme-gen uses OKLCH โ€” the perceptually uniform color space that ships in all modern browsers.

What this means for you: when salt-theme-gen adjusts lightness for dark mode or derives a hover state, the perceived brightness change is consistent. oklch(0.55 0.2 240) lightened to oklch(0.65 0.2 240) looks the same magnitude of change regardless of hue. In HSL, the same numeric change can look wildly different across colors.

The result: dark mode colors that actually look right, not just mathematically derived.

Three steps to use it anywhere

Step 1 โ€” Generate the theme:

const theme = generateTheme({ preset: 'ocean' });
Enter fullscreen mode Exit fullscreen mode

Step 2 โ€” Convert to CSS custom properties:

function modeToVars(mode) {
  const lines = [];
  const kebab = s => s.replace(/([A-Z])/g, '-$1').toLowerCase();

  for (const [k, v] of Object.entries(mode.colors))
    lines.push(`  --color-${kebab(k)}: ${v};`);
  for (const [k, v] of Object.entries(mode.spacing))
    lines.push(`  --space-${k}: ${v}px;`);
  for (const [k, v] of Object.entries(mode.radius))
    lines.push(`  --radius-${k}: ${v}px;`);
  for (const [k, v] of Object.entries(mode.fontSizes))
    lines.push(`  --text-${k}: ${v}px;`);

  return lines.join('\n');
}

const css = `
:root { ${modeToVars(theme.light)} }
:root[data-theme="dark"] { ${modeToVars(theme.dark)} }
`;
Enter fullscreen mode Exit fullscreen mode

Step 3 โ€” Inject into <head> and use in CSS:

.btn-primary {
  background: var(--color-primary);
  color:      var(--color-on-primary);
  padding:    var(--space-sm) var(--space-lg);
  border-radius: var(--radius-md);
}
Enter fullscreen mode Exit fullscreen mode

That's the whole pattern. Works in React, Next.js, Vue, Svelte, Angular, Astro, vanilla JS โ€” anything that can put a <style> tag in <head>.

20 built-in presets

You don't need to pick colors โ€” you pick a character:

Preset Character
ocean Deep blue, calm, professional
rose Warm, approachable, consumer
violet Creative, bold
emerald Fresh, growth
amber Energetic, warm
slate Neutral, minimal
midnight Dark-first, developer

...plus 13 more: ruby, cobalt, forest, sunset, arctic, copper, coral, sage, indigo, teal, gold, plum, crimson.

Or skip presets entirely and pass any hex:

generateTheme({ preset: '#6366f1' }) // your brand color
Enter fullscreen mode Exit fullscreen mode

Scale presets

Three options each for spacing, radius, and font size:

generateTheme({
  preset:   'ocean',
  spacing:  'compact',  // or 'default' | 'spacious'
  radius:   'rounded',  // or 'sharp' | 'default' | 'pill'
  fontSize: 'large',    // or 'compact' | 'default'
});
Enter fullscreen mode Exit fullscreen mode

A startup UI uses spacing: 'spacious' + radius: 'rounded'. A developer tool uses spacing: 'compact' + radius: 'sharp'. The personality of your UI comes from this combination, not just the color.

What's next in this series

This series covers every major framework and use case:

  • Dark mode with zero JavaScript flash (next article)
  • WCAG accessibility built-in โ€” what the 18 checks cover
  • React, Next.js, Vue, SvelteKit, Angular, Astro, Remix โ€” one article per framework
  • Tailwind CSS, React Native, Expo, Flutter, Storybook, CSS-in-JS, Sass
  • TypeScript integration โ€” typed theme objects, exhaustive switches
  • Using with Claude Code, Cursor, and v0.dev โ€” prompt templates

Install and follow along:

npm install salt-theme-gen
Enter fullscreen mode Exit fullscreen mode

Full documentation: learn.esalt.net/salt-theme-gen


Part of the **salt-theme-gen โ€” Design Tokens for Every Framework* series ยท Article 1 of 24*

Top comments (0)