Selector capture
Selector capture turns a selector into a value — a first-class piece of data you can store in a variable, pass to a function, or build selectors from — instead of live selector syntax attached to a rule.
Write it with selector( … ) in value position (no $ sigil). Its argument is a
selector list, the same one :is() takes:
$card: selector(.card);
$both: selector(.card, .panel); // a selector list is fine
Why selector()
CSS already has a selector() function, and it already means "this argument is a
selector": @supports selector(:has(a)) tests whether the browser supports a selector,
and so does supports(selector(…)) inside CSS if(). Selector capture reuses that name
instead of inventing new syntax, so there is no special sigil to learn and nothing a
CSS parser doesn't already recognise.
Jess extends it in two ways:
- In an ordinary value it captures. CSS only uses
selector()inside a support condition, where it is a test. Anywhere else in a value, Jess reads it as a captured selector you can keep. - It takes a selector list. CSS's
selector()takes a single complex selector. Jess accepts a comma-separated list, the same argument:is()and:where()take, soselector(.card, .panel)is one value holding both selectors.
Inside a support condition nothing changes: @supports selector(.a > .b) and
if(supports(selector(.a)): …) are still CSS's feature test, in every syntax.
What it's for
Once a selector is a value you can:
- Reuse it — store a selector once and use it as a selector in several places (see below).
- Pass it around — hand it to a mixin, or compare it in a guard
(
when (@s = selector(.a))). - Take it apart — it is a list with one item per selector (see below).
Selector functions that build new selectors from a captured one (append, nest, unify, replace) are not available yet.
A selector list value
The captured value is the same value & is in a .jess or .scss value, and the
same value Sass's selector.parse() returns: a comma list with one item per
selector, each item a space list of its compound selectors and combinators. So
selector(.card, .panel > h2) has two items, and its second is .panel, >, h2.
An item taken out of it is still a selector: interpolated into a selector, it is
placed as the whole list is (see below), so the item .panel > h2 glued after
.active is .active:is(.panel > h2).
@-use "#sass/list";
$cards: selector(.card, .panel > h2);
.debug {
count: $list.length($cards); // count: 2
second: $list.nth($cards, 2); // second: .panel > h2
all: $cards; // all: .card, .panel > h2
}
$for ($card of $cards) {
${card} { padding: 1rem; } // one rule per selector
}
Written in a plain value, it is the selector list, .card, .panel > h2, as & is.
Using a captured selector in another selector
Interpolate the variable where a selector goes. Every captured branch is used, and the nesting works as if you had written the list there:
$cards: selector(.card, .panel);
.page {
${cards} {
padding: 1rem;
}
}
With nested output (the default) that writes the branches under .page:
.page {
.card,
.panel {
padding: 1rem;
}
}
With collapseNesting the same source writes .page :is(.card, .panel) { padding: 1rem; }.
A & inside the capture stays a parent reference, resolved where the selector is
used: selector(&.active, & + .item) interpolated inside .tab refers to .tab.
Gluing a captured selector to other text
A captured selector interpolated next to other selector text is placed the way & is.
The same rule covers & itself used as a value, and the result of selector.parse():
- A name glued after it builds a name from each selector, as
&__titledoes: one rule per selector. - Anything else glued to it (a class before it, a pseudo-class after it) treats the
list as one unit,
:is(…), as&stands for its parent.
$cards: selector(.card, .panel);
${cards}__title { font-weight: bold; }
.theme-dark${cards} { color: white; }
${cards}:hover { outline: none; }
.card__title,
.panel__title {
font-weight: bold;
}
.theme-dark:is(.card, .panel) {
color: white;
}
:is(.card, .panel):hover {
outline: none;
}
A plain comma list is different: it holds text, not selectors, so wherever it stands
in a selector, glued to other text or on its own after a combinator, each item is
pasted in and the rule repeats for it. Only inside a pseudo-class's argument
does it stay one item of that argument: :not(:is(apple, satsuma)).
$fruits: apple, satsuma; // a plain list
$fruit-selectors: selector(apple, satsuma);
.fruit-${fruits} { color: orange; } // .fruit-apple, .fruit-satsuma
.fruit-${fruit-selectors} { color: orange; } // .fruit-:is(apple, satsuma)
.basket ${fruits} { color: green; } // .basket apple, .basket satsuma
.basket ${fruit-selectors} { color: green; } // .basket :is(apple, satsuma)
The second rule matches an element with the class fruit- that is also an apple or a
satsuma element: the class before a captured selector never becomes part of a name,
just as .fruit-& does not.
An argument that is not a selector list (selector(1px), selector()) is an error
at the argument. The rest of the stylesheet still parses, so an editor shows every
such error at once.