Skip to main content

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, so selector(.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 &__title does: 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.