Styles

A style is a named set of design tokens, colors and a few rules. Switch it, and the very same markup looks completely different: no class name changes, no new components.

Spruce CSS v3 ships eight style presets. The example below shows each of them on the same markup, and every component preview has a style picker to try them.

Minimal

Brutalist

Soft

Glass

Corporate

Playful

Editorial

Terminal

StyleDescription
minimalQuiet, monochrome and flat. Hairline borders, small radii, no decorative shadows; the content does the talking.
brutalistNeo-brutalism: thick black outlines, hard offset shadows, zero radius, loud yellow and pink, uppercase labels.
softSoft UI: generous rounding, pastel surfaces, pill-shaped buttons and large diffused shadows.
glassGlassmorphism: a soft pastel mesh backdrop seen through frosted, translucent and blurred surfaces.
corporateEnterprise and dense: sharp 2px corners, a confident blue, neutral grays, bottom-lined inputs and clear focus states.
playfulBubbly and vivid: rounded type, chunky 2px outlines, pill buttons with a pressable 3D lip and candy colors.
editorialMagazine and print: Fraunces serif headings over clean sans-serif text, paper tones, hairline rules, small-caps labels and a single ink-red accent.
terminalRetro command line: monospace everything, phosphor green on near-black, square corners, glowing text and prompt markers.

Turn On the Styles

Styles work through CSS custom properties, so you need the custom property mode. Then choose how you want to apply them.

Runtime: the data-spruce-style Attribute

Turn on the styles generator, and every preset is generated under an attribute selector:

@use 'sprucecss/scss/spruce' with (
  $settings: (
    'css-custom-properties': true,
  ),
  $generators: (
    'content': (
      'styles': true,
    ),
  ),
);
<html lang="en" data-spruce-style="brutalist">

Let your users choose: change the attribute with JavaScript and store the choice. Apply it in the <head> before the first paint to avoid a flash:

<script>
  const style = localStorage.getItem('preferred-style');
  if (style) document.documentElement.setAttribute('data-spruce-style', style);
</script>
document.querySelector('#style-select').addEventListener('change', (event) => {
  const style = event.target.value;

  if (style) {
    document.documentElement.setAttribute('data-spruce-style', style);
    localStorage.setItem('preferred-style', style);
  } else {
    document.documentElement.removeAttribute('data-spruce-style');
    localStorage.removeItem('preferred-style');
  }
});

Compile Time: a Default Style

If your project uses one style only, set it as the default. It is generated directly on :root, without the attribute (the generator can stay off):

@use 'sprucecss/scss/spruce' with (
  $settings: (
    'css-custom-properties': true,
    'styles': (
      'default': 'editorial',
    ),
  ),
);

Only Some Presets

Every preset adds CSS. Use the include list to generate only the ones you offer:

@use 'sprucecss/scss/spruce' with (
  $settings: (
    'css-custom-properties': true,
    'styles': (
      'include': ('minimal', 'glass'),
    ),
  ),
  $generators: (
    'content': ('styles': true),
  ),
);

Fonts

Some presets ship their own fonts from the font folder of the npm package (all under the SIL Open Font License): Brutalist uses Archivo and Archivo Black, with tuned vertical metrics so uppercase labels sit in the middle of buttons, and Editorial uses Fraunces (regular and italic) for its headings. The @font-face rules are generated only for the presets you generate, and their URLs start with the font-path setting (default: '../font/', relative to the css folder). Copy the files next to your CSS and point the setting at them, or set it to null to load the fonts yourself:

@use 'sprucecss/scss/spruce' with (
  $settings: (
    'css-custom-properties': true,
    'styles': (
      'font-path': '/assets/fonts/',
    ),
  ),
);

Prefer plain CSS? The spruce.complete.css (and .min.css) bundle in the npm package has every style, the dark mode and the opt-in utilities turned on.

Scoped Styles

The attribute works on any element, not just <html>. Use it for previews, themed sections or a single widget:

<section data-spruce-style="terminal">
    <h2>Command line</h2>
    <button class="btn btn--primary">Run</button>
</section>
  • The tokens (custom properties) cascade, so the nearest style wins.
  • The preset rules (like the brutalist button shadow) stop at the boundary of a nested scope that uses a different style.
  • The scope element gets the style's background, text color, font family and line height.

Dark Mode

Every preset has a dark palette (dark-colors) that applies together with the dark mode. The light colors are merged with the dark overrides, so a style keeps its identity in both modes. A preset without dark colors (like terminal) looks the same in both.

Create Your Own Style

Add a new entry to the $styles map. You can also overwrite any part of a built-in preset, because your map is deep-merged into the presets.

@use 'sprucecss/scss/spruce' with (
  $settings: ('css-custom-properties': true),
  $generators: ('content': ('styles': true)),
  $styles: (
    'sunset': (
      'label': 'Sunset',
      'description': 'Warm colors, rounded cards and serif headings.',
      'tokens': (
        'radius-sm': 0.5rem,
        'radius-lg': 1.25rem,
        'font-family-heading': #{Georgia, serif},
        'btn-padding': 0.8em 1.4em,
        'shadow-md': 0 8px 24px hsl(20deg 80% 40% / 18%),
      ),
      'colors': (
        'base': (
          'background': hsl(30deg 100% 98%),
          'primary': hsl(20deg 90% 50%),
        ),
        'btn': (
          'primary-background': hsl(20deg 90% 50%),
          'primary-background-hover': hsl(20deg 90% 44%),
        ),
      ),
      'dark-colors': (
        'base': (
          'background': hsl(20deg 20% 8%),
        ),
      ),
      'rules': (
        '.btn': ('text-transform': uppercase),
      ),
    ),
  ),
);
KeyDescription
label, descriptionMetadata for your docs and tools (not output).
tokensCustom property values set on the scope. Use a shorthand or any custom property name without the prefix.
colorsA map with the same structure as $colors. Only existing keys have an effect; you get a warning otherwise.
dark-colorsMerged over colors in dark mode.
rulesselector => declarations for what a token can't express. & is the scope element itself.
dark-rulesThe same, in dark mode only.
font-facesA list of @font-face descriptor maps. src is a .woff2 file name under the font-path setting; every other key is output as a descriptor (like ascent-override).

Shorthands

ShorthandSets
radius-smborder-radius-sm, border-radius (inline code and mark), pre-border-radius, btn-border-radius, form-control-border-radius, form-control-sm-border-radius, form-check-border-radius, details-border-radius
radius-mdborder-radius-md
radius-lgborder-radius-lg, dialog-border-radius
border-widthborder-width-base, btn-border-width, form-control-border-width, form-check-border-width, form-switch-border-width, details-border-width, dialog-border-width, table-border-width, divider-border-width

Extend the list with the $style-shorthands map.

Style Hooks

In custom property mode, every component value is exposed as a hook: a namespaced custom property you can set on any ancestor.

/* The generated button reads its padding from the hook, with the default as the fallback. */
.btn {
  --spruce-padding: var(--spruce-btn-padding, 0.75em 1em);
  padding: var(--spruce-padding);
}

So a style (or you, in plain CSS) can change one component without touching the others:

.marketing-section {
  --spruce-btn-padding: 1em 2em;
  --spruce-form-control-border-radius: 0;
}

The hook names follow the --{prefix}-{component}-{key} pattern. The components are: btn, btn-sm, btn-lg, form-control, form-control-sm, form-control-lg, form-control-color, form-check, form-check-sm, form-check-lg, form-switch, form-switch-sm, form-switch-lg, form-range, form-range-sm, form-range-lg, form-group, form-group-check, form-group-row, form-description, form-fieldset, table, table-sm, details, dialog, progress and divider. The keys are the keys of the related Sass map (like padding in $btn).

Only keys with a non-null default are hooks, because Spruce doesn't output a declaration for null values. Use rules for those (like text-transform or letter-spacing). Turn the hooks off with the style-hooks setting if you don't need them.

Make Your Components Style-Aware

Use the shared tokens in your own components and they follow every style and the dark mode:

@use 'sprucecss/scss/spruce' as *;

.card {
  background-color: color('surface');
  border: config('border-width-base', $display) solid color('border');
  border-radius: radius('lg');
  box-shadow: shadow('sm');
  padding: spacer-clamp('m', 'l');
}

Your project can also add its own color groups to the presets, like this site does for its hero and footer:

@use 'sprucecss/scss/spruce' with (
  $styles: (
    'brutalist': (
      'colors': ('footer': ('background': hsl(0deg 0% 0%))),
    ),
  ),
);

AI Skills

Every preset has an AI skill that describes its visual language, so coding assistants can design new screens in the same style.

Documentation

Learn about Spruce CSS through our extensive documentation.

Components

Explore our UI library of accessible components built with Spruce CSS.

Blocks and Pages

Full-width page blocks and complete page templates, ready to copy.

Find us on GitHub

Did you find a bug? Have an idea or a question? Please open an issue to help us develop the project.