The language

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).

example.scs
@let $spacing = 16px; @let $font = "Inter"; @let $weight = 600; .title { padding: $spacing; font-family: $font; font-weight: $weight; }
generated css
.title { padding: 16px; font-family: "Inter"; font-weight: 600; }

Arithmetic and units

Compatible units are required: 8px * 3 is 24px, 8px / 2px is 4, and 10px + 2rem is a compile error.

example.scs
@let $base = 8px; @let $large = $base * 3; @let $small = $base / 2; @let $ratio = $base / 2px; .a { padding: $large; margin: $small; line-height: $ratio; }
generated css
.a { padding: 24px; margin: 4px; line-height: 4; }

${...} — computing inside a css value

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.

example.scs
@let $spacing = 8px; .card { padding: $spacing ${$spacing * 4 / 2}; grid-area: 1 / 2 / 3 / 4; width: calc(100% - ${$spacing * 2}); }
generated css
.card { padding: 8px 16px; grid-area: 1 / 2 / 3 / 4; width: calc(100% - 16px); }

Scope

A variable declared inside a block shadows the outer one, and only inside that block.

example.scs
@let $spacing = 16px; @style card { @let $spacing = 24px; padding: $spacing; } .a {@use card;} .b {padding: $spacing;}
generated css
.a { padding: 24px; } .b { padding: 16px; }

2. Design tokens — @theme

@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.

example.scs
@theme { colors { primary: #6366f1; danger: #ef4444; } spacing { sm: 8px; md: 16px; } } .button { background: $colors.primary; padding: $spacing.sm $spacing.md; }
generated css
:root { --colors-primary: #6366f1; --colors-danger: #ef4444; --spacing-sm: 8px; --spacing-md: 16px; } .button { background: var(--colors-primary); padding: var(--spacing-sm) var(--spacing-md); }

Scoped themes

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.

example.scs
@let $base = 8px; @theme { colors {primary: #6366f1;} spacing {lg: ${$base * 3};} } @theme dark { colors {primary: #818cf8;} } .button {background: $colors.primary; padding: $spacing.lg;}
generated css
:root { --colors-primary: #6366f1; --spacing-lg: 24px; } [data-theme="dark"] { --colors-primary: #818cf8; } .button { background: var(--colors-primary); padding: var(--spacing-lg); }
live preview

Prefixing, and the opt-in optimization

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.

example.scs
@theme {colors {primary: #6366f1;}} .button {color: $colors.primary;}
generated css
:root { --mmo-colors-primary: #6366f1; } .button { color: var(--mmo-colors-primary); }
example.scs
@theme {colors {primary: #6366f1;}} .button {color: $colors.primary;}
generated css
:root { --colors-primary: #6366f1; } .button { color: #6366f1; }

3. Reusable styles — @style / @use

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.

example.scs
@style center { display: flex; align-items: center; justify-content: center; } @style size($width, $height) { width: $width; height: $height; } @style spacing($padding = 16px) {padding: $padding;} @style card { @use center; @use spacing; border-radius: 8px; } .avatar {@use size(48px, 48px);} .profile {@use card;} .tight {@use spacing(4px);}
generated css
.avatar { width: 48px; height: 48px; } .profile { display: flex; align-items: center; justify-content: center; padding: 16px; border-radius: 8px; } .tight { padding: 4px; }

4. Functions — @fn

A function generates a value, never css. Its body may contain @let, @if, @for and return.

example.scs
@fn spacing($n) { return $n * 4px; } @fn buttonColor($type) { @if $type == primary {return #6366f1;} @if $type == danger {return #ef4444;} return #64748b; } .a {padding: spacing(4); background: buttonColor(primary);} .b {background: buttonColor(danger);} .c {background: buttonColor(ghost);}
generated css
.a { padding: 16px; background: #6366f1; } .b { background: #ef4444; } .c { background: #64748b; }

Built-ins

lighten() darken() mix() rgba() px() rem() em() percent() round() floor() ceil() abs() length() nth() get() keys() quote() unquote() to-upper-case() if() …

example.scs
@let $brand = #6366f1; .a { color: darken($brand, 10%); background: lighten($brand, 30%); border-color: mix($brand, #ffffff); outline-color: rgba($brand, 0.5); width: rem(1.5); margin: round(12.7px); }
generated css
.a { color: #3438ed; background: #eff0fe; border-color: #b1b3f8; outline-color: rgba(99, 102, 241, 0.5); width: 1.5rem; margin: 13px; }
live preview

Css functions are preserved

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
@let $gap = 20px; .a { width: clamp(200px, 50vw, 800px); height: calc(100% - $gap); color: var(--brand, #fff); background: linear-gradient(135deg, #6366f1, #ef4444); }
generated css
.a { width: clamp(200px, 50vw, 800px); height: calc(100% - 20px); color: var(--brand, #fff); background: linear-gradient(135deg, #6366f1, #ef4444); }
example.scs
.a {padding: spacng(4);}
compilation error
Error: 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}).

example.scs
@theme {colors {primary: #6366f1, danger: #ef4444;}} @component button { padding: 8px 16px; border: none; border-radius: 6px; color: white; background: $colors.primary; cursor: pointer; &:hover {filter: brightness(1.1);} @variant danger {background: $colors.danger;} @variant outline { background: transparent; color: $colors.primary; border: 1px solid $colors.primary; } } @component .btn-link { background: none; border: none; text-decoration: underline; }
generated css
:root { --colors-primary: #6366f1; --colors-danger: #ef4444; } .button { padding: 8px 16px; border: none; border-radius: 6px; color: white; background: var(--colors-primary); cursor: pointer; } .button:hover { filter: brightness(1.1); } .button--danger { background: var(--colors-danger); } .button--outline { background: transparent; color: var(--colors-primary); border: 1px solid var(--colors-primary); } .btn-link { background: none; border: none; text-decoration: underline; }
live preview

Both templates are configurable, for a design system that prefixes its classes:

example.scs
@component button { padding: 8px; @variant primary {color: white;} }
generated css
.mmo-button { padding: 8px; } .mmo-button.is-primary { color: white; }

6. Nesting

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.

example.scs
.card, .panel { padding: 16px; .title {font-size: 20px;} &:hover {transform: scale(1.02);} > .body {color: #6b7280;} [data-state="on"] & {outline: 1px solid;} }
generated css
.card, .panel { padding: 16px; } .card .title, .panel .title { font-size: 20px; } .card:hover, .panel:hover { transform: scale(1.02); } .card > .body, .panel > .body { color: #6b7280; } [data-state="on"] .card, [data-state="on"] .panel { outline: 1px solid; }

7. Responsive — @screen

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.

example.scs
@config @screen.phone = 480px; @let $mobilePadding = 12px; @let $desktopPadding = 24px; .card { padding: $mobilePadding; @screen phone {padding: 16px;} @screen tablet {padding: $desktopPadding;} @screen 1200px {padding: 32px;} @screen (orientation: landscape) {aspect-ratio: 16 / 9;} } @screen tablet { .sidebar {display: block;} }
generated css
.card { padding: 12px; } @media (min-width: 480px) { .card { padding: 16px; } } @media (min-width: 768px) { .card { padding: 24px; } .sidebar { display: block; } } @media (min-width: 1200px) { .card { padding: 32px; } } @media (orientation: landscape) { .card { aspect-ratio: 16 / 9; } }

8. Conditions

@if / @else if / @else decide what is generated. Operators: == != < > <= >=, and, or, not. Only false and null are falsy.

example.scs
@let $rounded = true; @let $flat = false; .card { @if $rounded { border-radius: 8px; } @else { border-radius: 0; } @if $flat { box-shadow: none; } @else if $rounded and (not $flat) { box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); } }
generated css
.card { border-radius: 8px; box-shadow: 0 1px 2px rgba(0, 0, 0, 0.2); }

9. Loops and dynamic selectors

@for runs over a numeric range or over a collection, and the loop variable is interpolated into selectors, values and strings.

example.scs
@fn spacing($n) {return $n * 4px;} @for $i in 1..4 { .mt-${$i} {margin-top: spacing($i);} }
generated css
.mt-1 { margin-top: 4px; } .mt-2 { margin-top: 8px; } .mt-3 { margin-top: 12px; } .mt-4 { margin-top: 16px; }
example.scs
@let $sizes = [4, 8, 16]; @let $labels = (sm: small, md: medium); @for $index:$size in $sizes { .p-${$index} {padding: ${$size}px;} } @for $key:$label in $labels { .is-${$key}::after {content: "${$label}";} }
generated css
.p-0 { padding: 4px; } .p-1 { padding: 8px; } .p-2 { padding: 16px; } .is-sm::after { content: "small"; } .is-md::after { content: "medium"; }

10. Emission order — @order

Every rule is at order 0 and emitted in source order. @order moves a block earlier or later; higher values are emitted last.

example.scs
@order 100 {.utility {margin: 0 !important;}} .card {padding: 16px;} @order -100 { .reset {margin: 0; padding: 0;} }
generated css
.reset { margin: 0; padding: 0; } .card { padding: 16px; } .utility { margin: 0 !important; }

Inside a rule, @order reorders the declarations of that rule rather than emitting a second one — the framework example of the norm relies on it:

example.scs
@component button { padding: 8px; background: #6366f1; @order -1 {border: none;} }
generated css
.button { border: none; padding: 8px; background: #6366f1; }

11. Namespaces and imports

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:

example.scs
@import "theme.scs"; @import "forms.scs"; .field { @use forms::field(8px); width: ${forms::double(120)}px; transition: border-color $theme::transition; input {@use forms::input;} }
generated css
:root { --colors-primary: #6366f1; --colors-primary-dark: #4f46e5; --colors-danger: #ef4444; --colors-surface: #ffffff; --colors-text: #111827; --colors-muted: #6b7280; --spacing-xs: 4px; --spacing-sm: 8px; --spacing-md: 16px; --spacing-lg: 24px; --radius-sm: 4px; --radius-md: 8px; } [data-theme="dark"] { --colors-primary: #818cf8; --colors-surface: #111827; --colors-text: #f9fafb; } .field { display: flex; flex-direction: column; gap: 8px; width: 240px; transition: border-color 150ms ease; } .field input { border: 1px solid var(--colors-muted); border-radius: var(--radius-sm); padding: var(--spacing-sm) var(--spacing-md); transition: border-color 150ms ease; }

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.

WrittenMeans
@use centera @style of this file
@use lib::centerthe @style lib.scs declares
step(2)an @fn of this file, else a built-in, else a css function
lib::step(2)the @fn lib.scs declares
$guttera variable of this file, or of an enclosing block
$lib::gutterthe top-level @let of lib.scs
$color.branda 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.

example.scs
@import "theme.scs"; @import "forms.scs" as ui; .a {@use ui::input;}
generated css
:root { --colors-primary: #6366f1; --colors-primary-dark: #4f46e5; --colors-danger: #ef4444; --colors-surface: #ffffff; --colors-text: #111827; --colors-muted: #6b7280; --spacing-xs: 4px; --spacing-sm: 8px; --spacing-md: 16px; --spacing-lg: 24px; --radius-sm: 4px; --radius-md: 8px; } [data-theme="dark"] { --colors-primary: #818cf8; --colors-surface: #111827; --colors-text: #f9fafb; } .a { border: 1px solid var(--colors-muted); border-radius: var(--radius-sm); padding: var(--spacing-sm) var(--spacing-md); transition: border-color 150ms ease; }
example.scs
@import "theme.scs"; @import "forms.scs" as self; .a {@use input;}
generated css
:root { --colors-primary: #6366f1; --colors-primary-dark: #4f46e5; --colors-danger: #ef4444; --colors-surface: #ffffff; --colors-text: #111827; --colors-muted: #6b7280; --spacing-xs: 4px; --spacing-sm: 8px; --spacing-md: 16px; --spacing-lg: 24px; --radius-sm: 4px; --radius-md: 8px; } [data-theme="dark"] { --colors-primary: #818cf8; --colors-surface: #111827; --colors-text: #f9fafb; } .a { border: 1px solid var(--colors-muted); border-radius: var(--radius-sm); padding: var(--spacing-sm) var(--spacing-md); transition: border-color 150ms ease; }

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 error
Error: 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.

example.scs
@let $family = "Inter"; @font-face { font-family: $family; src: url("/fonts/inter.woff2") format("woff2"); } @keyframes spin { from {transform: rotate(0deg);} to {transform: rotate(360deg);} } @supports (display: grid) { .grid {display: grid;} }
generated css
@font-face { font-family: "Inter"; src: url("/fonts/inter.woff2") format("woff2"); } @keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } @supports (display: grid) { .grid { display: grid; } }

13. Output

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.

example.scs
@theme {colors {primary: #6366f1;}} .a {color: $colors.primary; padding: 8px;} .a {padding: 16px;} @screen tablet {.a {padding: 24px;}}
generated css
:root{--colors-primary:#6366f1}.a{color:var(--colors-primary);padding:16px}@media (min-width: 768px){.a{padding:24px}}

14. Errors

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.

example.scs
.card { padding: $spacing; }
compilation error
Error: undefined variable [$spacing]. at example.scs:2:12 2 | padding: $spacing; | ^^^^^^^^
example.scs
@let $x = 10px + 2rem; .a {width: $x;}
compilation error
Error: cannot compute [10px + 2rem], units are incompatible. at example.scs:1:11 1 | @let $x = 10px + 2rem; | ^^^^
example.scs
@style card {@use panel;} @style panel {@use container;} @style container {@use card;} .x {@use card;}
compilation error
Error: circular dependency: style example::card -> style example::panel -> style example::container -> style example::card. at example.scs:3:19 3 | @style container {@use card;} | ^^^^

Tags

<#simple css> <#scs> <#css> <#language>