Lists, maps, and collections
Collections are Jess's map shape for design tokens and structured values. They look like small CSS blocks, read like objects, and work with the same value model Jess uses everywhere else.
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;
}