Transpiling CSS

The styles key compiles SCSS/Sass with Dart Sass โ€” the fastest, most up-to-date Sass implementation.

A single stylesheet

{
  "styles": [
    {
      "in": "src/scss/index.scss",
      "out": "dist/css/styles.css",
      "options": {
        "sourcemap": true,
        "minify": true,
        "justMinified": false
      }
    }
  ]
}
  • in โ€” a .scss/.sass entry file, an array of entry files, or a glob pattern.
  • out โ€” the output CSS file, a directory when in has multiple entries, or a template naming one output per entry.

Options

Option Meaning
sourcemap Emit a source map (non-minified output only). Default false.
minify Also emit a minified CSS file (via esbuild). Default false.
justMinified Emit only the minified file. Default false.
tokenPaths Directories of JSON design tokens to expose to Sass.
tokenOutput variables (default) or map.
resolveAliases Resolve {path.to.token} references. Default true.

Tip

Like scripts, minify: true gives you both styles.css and styles.min.css in one build โ€” readable CSS for dev, minified for production.

Multiple stylesheets

{
  "styles": [
    { "in": "src/scss/main.scss", "out": "dist/css/styles.css",
      "options": { "sourcemap": true, "minify": true } },
    { "in": "src/scss/admin.scss", "out": "dist/css/admin.css",
      "options": { "minify": true } }
  ]
}

Globs and multiple entry files

in also accepts a glob pattern or an array of entry files โ€” handy when every top-level stylesheet should become its own CSS file (theme sections, per-page styles, โ€ฆ):

{
  "styles": [
    { "in": "src/scss/*.scss", "out": "dist/css/",
      "options": { "minify": true } }
  ]
}

Each matched file compiles separately to dist/css/<name>.css. Sass partials (_*.scss) are skipped โ€” they're imports, not entry points. Arrays and globs mix freely:

{ "in": ["src/scss/main.scss", "src/scss/pages/*.scss"], "out": "dist/css/" }

Brace alternates count as a glob on their own, so a pattern needs no * to match "whichever extension this entry happens to use":

{ "in": "src/scss/pages/*.{scss,sass,css}", "out": "dist/css/" }

Note

With more than one entry file, out must be a directory. Output names come from the input's basename, so two entries named main.scss in different directories would overwrite each other. Glob patterns always use / as the separator, even on Windows.

One stylesheet per component directory

Libraries of components usually give each component a directory, which makes every entry point index.* โ€” and by the basename rule above, every one of them would compile to index.css and overwrite the last. A glob-matched index.* is named after the directory holding it instead:

{ "in": "src/elements/*/index.{scss,sass,css}", "out": "dist/css/" }
src/elements/accordion/index.scss  โ†’  dist/css/accordion.css
src/elements/tabs/index.scss       โ†’  dist/css/tabs.css

Add a component directory, get a stylesheet โ€” no config change. The rename only applies to entries a glob matched: a literal "in": "src/scss/index.scss" still writes index.css, and an explicit out file path always wins.

The name is placed relative to the glob's static prefix โ€” everything before the first *, {โ€ฆ} or other magic segment. Above that prefix is src/elements, which every match shares, so nothing is left to nest under and the output is flat. Widen the glob and the part it no longer pins down is kept, so same-named components stay apart instead of overwriting each other:

{ "in": "src/*/accordion/index.scss", "out": "dist/css/" }
src/blocks/accordion/index.scss    โ†’  dist/css/blocks/accordion.css
src/elements/accordion/index.scss  โ†’  dist/css/elements/accordion.css

Note

The prefix comes from the pattern, not from what matched, so the layout doesn't shift when you add or remove a component. This is the one place styles nest โ€” non-index entries still flatten to their basename, per the rule above.

Naming outputs yourself

The index.* rule only rescues entry points actually named index. Every other glob match still flattens to its own basename, so src/elements/*/theme.scss writes theme.css once per component and the last one wins. When you need a different name, out can be a template:

  • {{dir}} โ€” the match's directory, relative to the glob's static prefix (the same name an index.* entry would get)
  • {{name}} โ€” the match's basename without extension
{ "in": "src/elements/*/theme.scss", "out": "dist/css/{{dir}}-{{name}}.css" }
src/elements/accordion/theme.scss  โ†’  dist/css/accordion-theme.css
src/elements/tabs/theme.scss       โ†’  dist/css/tabs-theme.css

One output per match instead of one shared output. Tokens may carry spaces ({{ dir }}) and can appear in directory segments too, so "out": "dist/css/{{dir}}/theme.css" writes dist/css/accordion/theme.css. A templated out is exempt from the "more than one entry file needs a directory" rule โ€” it already resolves to a different file per entry.

For a literal entry, {{dir}} is that entry's own directory name, so mixed arrays keep working:

{ "in": ["src/scss/main.scss", "src/elements/accordion/index.scss"], "out": "dist/css/{{dir}}.css" }
src/scss/main.scss                 โ†’  dist/css/scss.css
src/elements/accordion/index.scss  โ†’  dist/css/accordion.css

Note

A template wins over the index.* rename โ€” you named the outputs, so nothing renames them behind your back. A template that can't tell two matches apart ("out": "dist/css/{{name}}.css" across component directories) collides just like a plain directory out would โ€” that's what {{dir}} is for. Scripts take the same templates โ€” see Transpiling JS.

Resolving imports

node_modules is on the include path by default, so you can @use packages directly:

@use "some-design-system/scss/base";

If you set includePaths at the top level of your config, include node_modules yourself โ€” the setting replaces the default rather than adding to it.

Design tokens

Define your tokens once as JSON and @use them straight from SCSS via the token: prefix. Both W3C DTCG and Style Dictionary formats are auto-detected.

Given src/tokens/colors.json:

{
  "color": {
    "$type": "color",
    "primary": { "$value": "#0066cc" },
    "secondary": { "$value": "#ff6600" },
    "link": { "$value": "{color.primary}" }
  }
}

Point tokenPaths at the directory:

{
  "styles": [
    { "in": "src/scss/index.scss", "out": "dist/css/styles.css",
      "options": { "tokenPaths": ["src/tokens"] } }
  ]
}

Then use them as flat variables:

@use "token:colors" as c;

.btn { color: c.$color-primary; }
.btn:hover { color: c.$color-secondary; }
a { color: c.$color-link; } // resolved from {color.primary} โ†’ #0066cc

Prefer Sass maps? Set "tokenOutput": "map":

@use "sass:map";
@use "token:colors" as c;

.btn { color: map.get(c.$color, primary); }

Note

Design tokens keep a single source of truth for your color/spacing/typography scales, shared across Sass here and anything else that reads the same JSON โ€” no hand-maintained variable file to drift out of sync.

Need Tailwind or another PostCSS plugin instead of (or alongside) Sass? See PostCSS & Tailwind.

Next: Templating HTML.