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/.sassentry file, an array of entry files, or a glob pattern.outโ the output CSS file, a directory wheninhas 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 anindex.*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.