Sass (SCSS) compatibility
This doc explains how Jess parses and compiles SCSS (Sass’s CSS-like syntax), and how Sass-only constructs are mapped into Jess’s AST and (when possible) into Jess-native semantics.
Jess’s goal is to make Sass input parseable, while preferring Jess-native output semantics where they’re strictly better (for example, maps).
What SCSS adds beyond CSS
Sass extends CSS with (at least) the following features:
- Variables:
$var: value;(Sass variables) - Interpolation:
#{...}in selectors, property names, and values (Interpolation) - Mixins/includes/content blocks:
@mixin,@include,@content,using (...)(Mixins) - Control directives:
@if,@for,@each,@while,@error,@warn,@debug(Control directives) - Module system:
@use,@forward, configuration viawith (...)(use, forward) - Maps and lists: including built-in helpers like
map.get()(Maps, sass:map) - Extend + placeholders:
@extendand%placeholders(extend, placeholders) - @at-root: explicit control over nesting output (at-root)
- Nested declarations (“property nesting”) (declarations)
At-rule mapping: Sass → Jess
Jess keeps compiler-level at-rules prefixed with - to avoid collisions with the evolving CSS standard library.
@use (stylesheets) → @-compose
Sass:
@use './theme' as theme;
.box { color: theme.$primary-color; }
Jess equivalent:
@-compose './theme' as theme;
.box { color: $theme.primary-color; }
Jess’s module import node is StyleImport (see packages/core/src/tree/import-style.ts). It supports namespaces, configuration, and export/protection flags.
@use (scripts) → @-use / @-from
In Jess, @-use is the Sass-module-style namespace form for JS/TS/JSON modules. Like Sass, it can infer the namespace from the module name when as is omitted. @-from is the ESM-style form for named imports or import * as ns namespace imports (see packages/core/src/tree/import-js.ts).
When parsing SCSS, Jess will treat @use as a stylesheet import by default, but if the @use path clearly targets a script module (for example ./tokens.js, ./tokens.ts, ./tokens.json), it will parse to a JS import node instead.
@forward (SCSS) → @-export
Jess models Sass forwarding semantics with @-export (see At-rules).
Sass maps → Jess collections
Sass maps are syntactically awkward compared to CSS rules. Jess maps them into the Collection node, which is a “map-like Rules” structure.
Sass:
$font-weights: ("regular": 400, "medium": 500, "bold": 700);
Jess AST shape (conceptual):
$font-weights: {
regular: 400;
medium: 500;
bold: 700;
}
Key normalization rules
- Quoted string keys with no interpolation are treated as plain keys.
"regular"becomesregular(no quotes) in the correspondingDeclarationname.
- More complex Sass map keys (colors, lists, etc.) are supported by Jess collections, but the SCSS mapping rules depend on how the key can be represented as a stable collection key (see Namespaces, collections, and maps).
map-get() and map.get() → Reference lookups
Sass commonly accesses map keys via:
@use "sass:map";
.box {
font-weight: map.get($font-weights, "medium");
}
Jess does not treat this as a normal function call. Instead, the SCSS parser desugars:
map-get($map, $key[, ...])map.get($map, $key[, ...])
into a Reference lookup chain:
- Evaluate the map expression (often
$font-weights) - Look up the key(s) as nested property/index accesses
This is the same core mechanism Jess uses for $theme.colors.primary and array/map-like access.
Slash division and arithmetic
Historically, Sass treated / as either:
- division
- or a plain slash separator in a value
The old Sass rules were heuristic and inconsistent because Sass also overloaded () for grouping, lists, and maps.
Jess does not try to preserve that whole ambiguity set when importing SCSS.
Jess supports the clear arithmetic forms:
math.div(...)- isolated parenthesized arithmetic like
(15px/30px)
Jess does not try to infer division from every legacy Sass slash form in general value position. If a slash form is ambiguous, Jess prefers to keep it as a slash-separated value rather than guess.
This aligns with Jess’s own language model:
- real math should be explicit
- imported SCSS arithmetic should only be recognized when the syntax is clearly arithmetic
- native Jess expressions use
$(...)
SCSS interpolation mapping
Jess distinguishes:
Reference:$varExpression:$(...)Referencewithrole: 'ident':$[ident]
#{$...} in selector/property names
When SCSS interpolation appears in selector or property-name positions:
#{$ident}(single variable reference) maps to$[ident]- More complex interpolation expressions are rewritten through a generated private temp variable (
_tmp_<n>), then referenced as$[_tmp_<n>]
This keeps selector/property interpolation identifier-only while still supporting complex expressions.
#{$...} in values/strings
In value/string contexts, SCSS interpolation may map to:
$[ident]for simple identifier interpolation$(...)for expression interpolation
For example, #{$color} can become $($color) when expression semantics are needed.
Interpolation in mixin names
SCSS does not allow interpolation in mixin names. Jess follows that behavior:
@include foo-#{$bar}()is rejected@mixin foo-#{$bar}is rejected
Placeholder selectors (%foo)
Sass placeholder selectors don’t emit CSS unless they’re extended.
Jess parses:
%fooas selector name\foo
This name conversion makes placeholders syntactically distinguishable from normal selectors while still being a selector-like key in the registry.
Placeholder visibility (current state)
Sass placeholders are “silent” until extended. In Jess today, placeholder parsing is supported, but full Sass-equivalent placeholder visibility behavior remains partial. Use this area with extra verification in migration-heavy code paths.
Cross-module behavior
Sass’s module system allows extending placeholders across module boundaries, with private placeholders (leading _ or -) restricted to their defining module (see Sass’s placeholder docs above). Jess aims for parity here, but behavior is still being finalized.
Variable flags: !global and !default
Sass supports !global and !default on variable assignments (variables).
Jess’s SCSS support intentionally does not implement Sass’s full !global shadowing model. Instead, SCSS !global will be treated as “set existing / do not shadow” (Jess’s setDefined behavior), which is equivalent to !global unless the user has already shadowed the variable in a narrower scope—in that case, the user must refactor the source for clarity.
!default exists in Sass primarily to support module configuration (@use ... with (...)). Jess will align with Sass’s @use configuration behavior by ensuring configurable variables are explicitly marked and handled accordingly.