The pain this guide solves

Your _variables.scss has 80 hardcoded color values someone added over three years. Half of them are never used. Dark mode is a separate _dark.scss that nobody updates. Changing the primary color means grepping through a dozen partials and hoping nothing was hardcoded outside the variables file.

salt-theme-gen with Sass/SCSS

Two approaches

Both work. Use whichever fits your project:

ApproachHowBest for
CSS variables (recommended)Generate a .css file, import in SassAny project — Sass can use var()
Sass variablesGenerate a _tokens.scss partial, import in SassProjects that need Sass $variable syntax

The CSS variable approach is simpler and gives you dark mode for free. The Sass variable approach generates $primary, $spacing-md, etc. — familiar to teams that work heavily in Sass.


Generate a theme.css file from the token script (see the Vanilla JS guide) and import it in your main Sass file:

// src/main.scss
@use 'sass:math';

// Import generated CSS variables
@import './theme.css';  // or link it in <head>

// Your Sass — references CSS vars
body {
  background-color: var(--color-background);
  color: var(--color-text);
  font-size: var(--text-md);
  font-family: system-ui, -apple-system, sans-serif;
  line-height: 1.6;
}

.btn {
  background: var(--color-primary);
  color: var(--color-on-primary);
  border: none;
  border-radius: var(--radius-md);
  padding: var(--space-sm) var(--space-lg);
  font-size: var(--text-md);
  font-weight: 600;
  cursor: pointer;
  transition: background 0.15s;

  &:hover   { background: var(--state-primary-hover); }
  &:active  { background: var(--state-primary-pressed); }
  &:focus-visible {
    outline: 2px solid var(--state-primary-focused);
    outline-offset: 2px;
  }
  &:disabled {
    background: var(--state-primary-disabled);
    cursor: not-allowed;
  }
}

CSS variables are first-class in Sass — you can use them anywhere you would use a hardcoded value. Dark mode works the same way as in any other framework: [data-theme="dark"] or @media (prefers-color-scheme: dark) in the generated CSS file handles the switch.


Approach 2 — Generated Sass variables

For teams that prefer $primary over var(--color-primary), generate a _tokens.scss partial at build time.

Generation script

Create scripts/generate-sass-tokens.mjs:

import { generateTheme } from 'salt-theme-gen';
import { writeFileSync, mkdirSync } from 'node:fs';

const theme = generateTheme({
  preset: 'ocean',
  spacing: 'default',
  radius: 'default',
  fontSize: 'default',
});

function kebab(str) {
  return str.replace(/([A-Z])/g, '-$1').toLowerCase();
}

function sassValue(val) {
  // Keep OKLCH as-is for modern Sass + browsers that support it
  // Or convert to hex if you need wider browser support
  return val;
}

function colorsBlock(mode, prefix) {
  const lines = [`// ${prefix} colors`];

  for (const [k, v] of Object.entries(mode.colors))
    lines.push(`$${prefix}-${kebab(k)}: ${sassValue(v)};`);

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

function statesBlock(mode, prefix) {
  const lines = [`\n// ${prefix} states`];

  for (const [intent, states] of Object.entries(mode.states))
    for (const [state, val] of Object.entries(states))
      lines.push(`$${prefix}-state-${intent}-${state}: ${sassValue(val)};`);

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

function scalesBlock(mode) {
  const spacing = Object.entries(mode.spacing)
    .map(([k, v]) => `$spacing-${k}: ${v}px;`)
    .join('\n');

  const radius = Object.entries(mode.radius)
    .map(([k, v]) => `$radius-${k}: ${v}px;`)
    .join('\n');

  const fontSizes = Object.entries(mode.fontSizes)
    .map(([k, v]) => `$font-${k}: ${v}px;`)
    .join('\n');

  const surfaceElevation = Object.entries(mode.surfaceElevation)
    .map(([k, v]) => `$surface-${k}: ${sassValue(v)};`)
    .join('\n');

  return [
    '// Spacing',
    spacing,
    '\n// Radius',
    radius,
    '\n// Font sizes',
    fontSizes,
    '\n// Surface elevation',
    surfaceElevation,
  ].join('\n');
}

// Build Sass map for color iteration
function colorsMap(mode, name) {
  const entries = Object.entries(mode.colors)
    .map(([k, v]) => `  '${kebab(k)}': ${sassValue(v)}`)
    .join(',\n');

  return `$${name}: (\n${entries}\n);`;
}

const sass = [
  '// Generated by salt-theme-gen — do not edit manually',
  '// Run: node scripts/generate-sass-tokens.mjs',
  '',
  colorsBlock(theme.light, 'light'),
  statesBlock(theme.light, 'light'),
  '',
  colorsBlock(theme.dark, 'dark'),
  statesBlock(theme.dark, 'dark'),
  '',
  scalesBlock(theme.light),
  '',
  '// Sass maps (for iteration)',
  colorsMap(theme.light, 'light-colors'),
  '',
  colorsMap(theme.dark, 'dark-colors'),
].join('\n');

mkdirSync('src/styles', { recursive: true });
writeFileSync('src/styles/_tokens.scss', sass, 'utf8');
console.log('✓ src/styles/_tokens.scss written');

Run it:

node scripts/generate-sass-tokens.mjs

Add to package.json:

{
  "scripts": {
    "build:tokens": "node scripts/generate-sass-tokens.mjs",
    "build": "npm run build:tokens && vite build",
    "dev": "npm run build:tokens && vite dev"
  }
}

Using generated Sass variables

Import and use

// src/styles/main.scss
@use './tokens' as t;

body {
  background-color: t.$light-background;
  color: t.$light-text;
  font-size: t.$font-md;
  font-family: system-ui, -apple-system, sans-serif;
}

.btn-primary {
  background: t.$light-primary;
  color: t.$light-on-primary;
  border: none;
  border-radius: t.$radius-md;
  padding: t.$spacing-sm t.$spacing-lg;

  &:hover { background: t.$light-state-primary-hover; }
}

Dark mode with Sass variables

With Sass variables, you handle dark mode with a class or data attribute selector:

@use './tokens' as t;

:root {
  --color-background: #{t.$light-background};
  --color-primary:    #{t.$light-primary};
  --color-text:       #{t.$light-text};
  // ... all tokens as CSS vars using Sass values
}

[data-theme='dark'] {
  --color-background: #{t.$dark-background};
  --color-primary:    #{t.$dark-primary};
  --color-text:       #{t.$dark-text};
}

body {
  background-color: var(--color-background);
  color: var(--color-text);
}

This pattern uses Sass variables to populate CSS custom properties — you get the best of both worlds. Sass variables for the generation source of truth, CSS variables for runtime dark mode switching.


Sass maps for iteration

The generated $light-colors and $dark-colors maps let you generate utility classes programmatically:

@use './tokens' as t;

// Generate .text-{color} and .bg-{color} for each intent color
@each $name, $value in t.$light-colors {
  .text-#{$name} { color: $value; }
  .bg-#{$name}   { background-color: $value; }
}

// Generate .p-{size} spacing utilities
$spacing-map: (
  'xs':  t.$spacing-xs,
  'sm':  t.$spacing-sm,
  'md':  t.$spacing-md,
  'lg':  t.$spacing-lg,
  'xl':  t.$spacing-xl,
  'xxl': t.$spacing-xxl,
);

@each $name, $value in $spacing-map {
  .p-#{$name}  { padding: $value; }
  .px-#{$name} { padding-left: $value; padding-right: $value; }
  .py-#{$name} { padding-top: $value; padding-bottom: $value; }
  .gap-#{$name}{ gap: $value; }
}

Component patterns with Sass

Button with all states

// components/_button.scss
@use '../styles/tokens' as t;

.btn {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border: none;
  cursor: pointer;
  font-weight: 600;
  transition: background 0.15s;
  border-radius: t.$radius-md;
  font-size: t.$font-md;
  padding: t.$spacing-sm t.$spacing-lg;

  // Intent variants
  &--primary {
    background: t.$light-primary;
    color: t.$light-on-primary;
    &:hover   { background: t.$light-state-primary-hover; }
    &:active  { background: t.$light-state-primary-pressed; }
    &:focus-visible {
      outline: 2px solid t.$light-state-primary-focused;
      outline-offset: 2px;
    }
    &:disabled {
      background: t.$light-state-primary-disabled;
      cursor: not-allowed;
    }
  }

  &--danger {
    background: t.$light-danger;
    color: t.$light-on-danger;
    &:hover { background: t.$light-state-danger-hover; }
  }

  &--success {
    background: t.$light-success;
    color: t.$light-on-success;
    &:hover { background: t.$light-state-success-hover; }
  }
}

[data-theme='dark'] {
  .btn--primary {
    background: t.$dark-primary;
    color: t.$dark-on-primary;
    &:hover { background: t.$dark-state-primary-hover; }
  }
}

Card

// components/_card.scss
@use '../styles/tokens' as t;

.card {
  background: t.$light-surface-card;  // surfaceElevation.card
  border: 1px solid t.$light-border;
  border-radius: t.$radius-lg;
  padding: t.$spacing-xl;

  &__title {
    font-size: t.$font-lg;
    font-weight: 700;
    color: t.$light-text;
    margin: 0 0 t.$spacing-sm;
  }

  &__body {
    font-size: t.$font-md;
    color: t.$light-muted;
    line-height: 1.7;
    margin: 0;
  }
}

[data-theme='dark'] .card {
  background: t.$dark-surface-card;
  border-color: t.$dark-border;

  &__title { color: t.$dark-text; }
  &__body  { color: t.$dark-muted; }
}

Using with Vite

Vite’s built-in Sass support requires no additional configuration for this pattern. Install Sass:

npm install --save-dev sass

Import your main Sass file in main.ts or main.js:

import './src/styles/main.scss';

Vite compiles Sass automatically.

Auto-inject tokens in every file

To make _tokens.scss available in every Sass file without explicit @use:

// vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@use '/src/styles/tokens' as t;`,
      },
    },
  },
});

With additionalData, every .scss file automatically has access to t.$light-primary, t.$spacing-md, etc. — no @use needed in individual component files.


File structure summary

scripts/
└── generate-sass-tokens.mjs     ← writes _tokens.scss

src/
└── styles/
    ├── _tokens.scss             ← generated — do not edit
    ├── main.scss                ← imports tokens + base styles
    └── components/
        ├── _button.scss
        └── _card.scss

Checklist

  • node scripts/generate-sass-tokens.mjs writes _tokens.scss
  • package.json prebuild/predev runs token generation ✓
  • Component Sass files use @use '../styles/tokens' as t
  • vite.config.ts injects tokens via additionalData (optional) ✓
  • No hardcoded color values in any Sass file ✓
  • [data-theme='dark'] block updates all dark mode values ✓

Recommended: For most Sass projects, use the CSS variable approach (Approach 1) rather than Sass variables. CSS variables give you runtime dark mode switching with zero JavaScript — just toggle data-theme on <html>. Sass variables require re-generating styles or duplicating selectors for dark mode, which adds maintenance cost.

Live demo

Open this integration in StackBlitz — fully working, editable in your browser.

Open in StackBlitz →