Every construct of the norm, with the css the compiler really generates for it. The
complete written reference is on the
Reference page; to try your own source, use
the Playground.
1. Compile-time variables
A @let is replaced during compilation. It never becomes --spacing, and
never becomes var(--spacing).
In a declaration / * + - are css, not arithmetic — grid-area: 1 / 2 / 3 / 4
has to survive. Wrap an expression in ${...} when you do want it computed. The
#{...} spelling is accepted as a synonym.
@theme is the one construct that stays dynamic at runtime: groups are flattened into css
custom properties on :root, and a $path reference always compiles to a
var() — never to the value.
A named theme is scoped to a selector, so switching an attribute re-themes the page with no
recompilation. Token values are ordinary compile-time expressions.
varPrefix keeps a design system from colliding with another one.
resolveThemeValues inlines the token values instead — the stylesheet stops depending on
custom properties, and runtime theming stops working, which is why it is off by default.
A @style generates nothing by itself: it is a compile-time fragment, expanded where it is
used. It takes parameters, with optional defaults, and may use other styles.
A css function is emitted as written, arguments included — which is what makes calc()
work. Anything that is neither an @fn, a built-in nor a known css function is reported as an
undefined function, so a typo never silently becomes invalid css.
example.scs.a {padding: spacng(4);} compilation errorError: undefined function [spacng()] in [example].
at example.scs:1:14
1 | .a {padding: spacng(4);}
| ^^^^^^
5. Components and variants
A bare name becomes a class (componentSelector, .{name} by default), an
explicit selector is used as written. Variants are emitted as their own rule
(variantSelector, {selector}--{variant}).
Nesting is flattened for maximum browser compatibility. & is replaced by the parent
selector; without a & the child is a descendant. A selector list nests as a list.
A @screen target is a configured breakpoint name, a bare length (read as a
min-width), a media type or a full query. Responsive rules are always emitted after
the unwrapped ones, so a breakpoint never loses to the rule it overrides, whatever the source order.
A stylesheet of any size is several files, and reading one of them you need to know where a
@use, a function or a variable comes from. A file is a namespace, named after itself,
and a reference to another file says so. Here forms.scs declares input,
field and double(), and theme.scs the
$transition:
Imports are resolved at compile time and merged into a single stylesheet: a path is looked up in the
importing file's directory first, then in the configured paths, and a file imported twice
is merged once. @import decides what is compiled; :: decides what a name
refers to.
Written
Means
@use center
a @style of this file
@use lib::center
the @stylelib.scs declares
step(2)
an @fn of this file, else a built-in, else a css function
lib::step(2)
the @fnlib.scs declares
$gutter
a variable of this file, or of an enclosing block
$lib::gutter
the top-level @let of lib.scs
$color.brand
a theme token — global, never namespaced
Naming, renaming, merging
A file answers to its own name (lib.scs is lib, parts/buttons.scs
is buttons), unless it says otherwise with @namespace, or the import does with
as. as self merges a namespace into the importing file, for a prelude that
should read as if it had been written in place.
Naming the wrong one is a compile error that says what to write instead:
example.scs@import "theme.scs";
@import "forms.scs";
.a {@use input;} compilation errorError: undefined style [input] in [example]. It is declared in [forms]: write [forms::input].
at example.scs:4:5
4 | .a {@use input;}
| ^^^^
12. Css at-rules
At-rules the language does not interpret are still compiled — their prelude is interpolated, their
body compiled — then emitted as written. Keyframe selectors are never prefixed by the enclosing
rule.
The same source, with minify and optimize on: consecutive identical selectors
are merged and an overridden declaration is dropped. Distant rules are never merged — that would
move declarations across the rules between them and change the cascade.
Every failure is an eSCS carrying its source position; report() renders the
message, the position and a code frame. These three examples are compiled like the others — the
panel shows what the compiler answered.