Skip to main content

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 are not anonymous mixins

Collections and anonymous mixins both use blocks:

$mixin: @{
color: red;
}

a collection is data; an anonymous mixin is code.

That split is the rule:

  1. a collection cannot contain rulesets, mixin definitions, or at-rules,
  2. 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
  3. 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:

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

Numeric keys are not reachable by bracket

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
}
info

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:

  1. A ruleset might be CSS output, map data, or both.
  2. 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;
}