Lists, maps, and collections
Collections are Jess's map shape for design tokens and structured values. They
look like small CSS blocks marked with #, read like objects, and work with the
same value model Jess uses everywhere else. A collection is written #{ … }; a
bare { … } is a curly list, a list
passed as one value, which is a different thing (Sass writes both with parens).
A Jess collection
// theme.jess
$colors: #{
primary: #06c;
secondary: #e00;
tertiary: #000;
}
// app.jess
@-compose "./theme.jess";
.box {
color: $theme.colors.primary;
}
The composed module namespace theme is a collection, and colors is another
collection inside it. Use dot access for ordinary identifier keys.
Note that there is no ; after the collection's closing brace. A block-valued
assignment ends at its closing brace, so the semicolon is optional. See
block-valued assignments for the rule.
Script and data modules expose imported objects as collections. See Modules & imports.
Sub-collections
Collections can contain collections:
// theme.jess
$colors: #{
primary: #06c;
secondary: #e00;
tertiary: #{
light: #06c;
dark: #e00;
}
}
// app.jess
@-compose "./theme.jess";
.box {
color: $theme.colors.tertiary.light;
}
Collections and anonymous mixins both use blocks:
$mixin: @{
color: red;
}
a collection is data; an anonymous mixin is code.
That split is the rule:
- a collection cannot contain rulesets, mixin definitions, or at-rules,
- a collection cannot contain variable declarations either — a binding inside a block is scope machinery, so a block that declares variables is an anonymous mixin, not a collection, and
- a collection is for arbitrary key/value pairs, not CSS properties & values.
That keeps map functions honest: $a: 1 inside a collection would have to mean
both "key named $a" and "binding named a." Jess keeps those roles separate.
Collection keys are values
A collection key is a value, not a property name, and lookups match by value equality:
// the key here is the Keyword `small`
$m: #{
small: 4px;
}
A bare name is unambiguous because $ is what marks a variable: small is the
Keyword key, $small is the variable. So $m.small, $m[small], $m["small"],
and $m[$k] (where $k holds small) all reach the same entry.
The value model is the same one Sass maps use: keys retain their value type and
are compared by value, not by a serialized property-name string. Some different
spellings are equal under that comparison, such as a and "a" or 1 and
1.0.
A bare word is a Keyword, including a word that CSS can use as a named color.
Consequently, red: and [red]: are the same key. Color conversion happens only
when an operation requires it; [#c6538c]: authors an actual Color key.
Jess collection literals author the common CSS-object shape directly:
$font-weights: #{
regular: 400;
medium: 500;
bold: 700;
}
$key: accent;
$m: #{
[1px]: a;
[#c6538c]: b;
[$key]: c;
}
Computed keys are collection-only. They cannot be used as CSS property names.
Overlaying collections
Spread a collection to make a shallow, left-to-right overlay. The syntax follows JavaScript object spread while keeping Jess's semicolon-separated entries:
$defaults: #{
color: #06c;
radius: 4px;
}
$brand: #{
color: #c6538c;
}
$theme: #{
...$defaults;
...$brand;
radius: 6px;
}
$theme evaluates to #{ color: #c6538c; radius: 6px; }. A later value replaces
the earlier value without moving the key; a new key appends. An explicit
duplicate follows the same rule, so #{ a: 1; a: 2; } evaluates as #{ a: 2; }.
The jess/no-duplicate-collection-keys lint rule warns when duplicate explicit
keys in one literal are statically obvious. It never rejects the collection and
does not speculate about dynamic keys or keys introduced by a spread.
Spread is shallow: if both collections contain colors, the later colors
value replaces the earlier one. JavaScript consumers can use deep-merge from
the @jesscss/fns/sass/map entrypoint when a recursive overlay is needed. The
planned stylesheet spelling uses the #sass/map module — @-use "#sass/map"
loads without error today, but its members do not resolve
(jess#233).
Subscripts: the key's type decides
Jess also uses […] for positional access into a list, so one rule settles which
you get — the subscript's own type, never the receiver's:
| Subscript | Meaning |
|---|---|
a number — $x[0], $x[-1] | zero-based positional index; negative counts from the end |
a member key — $x[small], $x[$k], $x["a"] | key lookup by value equality |
$x[0] is positional whether $x turns out to be a list or a collection. A
subscript whose meaning shifted with the receiver's runtime type would be a
footgun; this way you can read the source and know.
If you use numbers as collection keys, numeric […] will not reach them because
a numeric subscript is always positional. Use map functions such as
map.get($m, 1px) for numeric keys.
Lists and arrays
Lists use bracket access:
// theme.jess
$sizes: 10px, 20px, 30px;
.box {
padding: $sizes[0]; // 10px
}
Negative indexes read from the end:
.box {
padding: $sizes[-1]; // 30px
}
Rulesets and mixins also support numeric indexed access to their emitted values:
.mixin() {
color: #06c;
color: #e00;
}
// Might be useful to access two values with the same key,
// since `$ > .mixin().color` would return the final value.
.box {
color-1: $ > .mixin[0]; // #06c
color-2: $ > .mixin[1]; // #e00
}
Why this reads better
Less maps
Less lets rulesets and mixins act like maps:
#colors() {
primary: #06c;
secondary: #e00;
}
.box {
color: #colors[primary];
background: #colors[secondary];
}
→
.box {
color: #06c;
background: #e00;
}
That gives Less a clever reuse story, but it blurs two jobs:
- A ruleset might be CSS output, map data, or both.
- Variable-looking keys can be hard to read, especially in forms like
@map[@my-var].
Sass maps
Sass maps are real data, but the syntax is separate from normal CSS-shaped blocks:
$font-weights: ("regular": 400, "medium": 500, "bold": 700);
Reads usually go through map-get or map.get:
value: map.get($font-weights, "medium"); // 500
value: map.get($font-weights, "bold"); // 700
Jess keeps the good part — maps are data — and gives the common case a shape that CSS and JavaScript developers already know:
$font-weights: #{
regular: 400;
medium: 500;
bold: 700;
}
value: $font-weights.regular; // 400
value: $font-weights.bold; // 700
That is the practical DX win: CSS-shaped data, JavaScript-shaped property access,
and bracket lookup when the key is a value. You do not have to pretend a ruleset
is data, and you do not have to wrap ordinary reads in map.get(...).
$state: active;
$colors: #{
active: #06c;
disabled: #999;
}
.button {
color: $colors[$state];
font-weight: $font-weights.bold;
}