Skip to main content

Selector compaction (:is() nesting)

When a &-less nested rule collapses onto its accumulated ancestor, Jess factors the common ancestor out once and wraps each multi-branch side in a single :is(...) — instead of repeating the whole prefix or cartesian-expanding it into one row per combination.

The rule

Joining a &-less descendant onto its ancestor emits the ancestor once, then wraps each side in :is(...) only if it is a multi-branch comma list (a single selector joins plainly):

InputOutput
.a, .b { .c {…} }:is(.a, .b) .c {…}
.a { .c, .d {…} }.a :is(.c, .d) {…}
.a, .b { .c, .d {…} }:is(.a, .b) :is(.c, .d) {…} — one row
.a, .b { & .c {…} }.a .c, .b .c {…}& nesting cartesian-expands
.a, .b {…}.a, .b {…} — a rule's own header stays a plain list

A &-based child is a different path: each & substitutes over the full cartesian ancestor list (no :is()).

Worked example

#first #deux {
#fourth, #five, #six {
.seven, .eight > #nine { margin: 0; }
#ten { padding: 0; }
}
}
#first #deux :is(#fourth, #five, #six) :is(.seven, .eight > #nine) {
margin: 0;
}
#first #deux :is(#fourth, #five, #six) #ten {
padding: 0;
}

The common ancestor is written once per child rule instead of exploding into 3 × 2 selectors. This is the same compaction as the Less 5.x engine — see the Less Selector Compaction page for the details, and Extend for the separate extend-driven :is() grafting.

Specificity and :is() grouping (nesting & extend)

Folding branches into :is(...) — from this nesting collapse or from extend's :is() grafting — changes how a browser scores the selector, because :is() does not score as zero. Per the CSS spec, the specificity of an :is() is the specificity of its most specific argument (Selectors Level 4 — specificity). So a low-specificity branch grouped with a high-specificity one inherits the group's maximum: :is(.a, #b) scores as the ID #b(1,0,0) — for both branches.

Nesting collapse carries that group score into the join (flattened output):

.a, #b {
.c { color: red; }
}
:is(.a, #b) .c {
color: red;
}

:is(.a, #b) scores (1,0,0), so the whole selector scores (1,1,0) — the .a .c match now carries ID-level weight it would not have on its own. Extend's partial-match grafting behaves the same way: .a > .c extended by #b renders .a > :is(.c, #b), also (1,1,0).

Migration note vs. less.js 4.x. 4.x expanded these into a comma-separated cascade, each row keeping its own specificity; 5.x/Jess groups them into one :is() scored at the maximum:

Source4.x output (per-row specificity)5.x output (group specificity)
.a, #b { .c {} }.a .c (0,2,0), #b .c (1,1,0):is(.a, #b) .c — both (1,1,0)
.a > .c {} + #b:extend(.c all).a > .c (0,2,0), .a > #b (1,1,0).a > :is(.c, #b) — both (1,1,0)

When the grouped branches share specificity (the common all-classes case, :is(.a, .b)) nothing changes — the shift is observable only when branches of different specificity are grouped, where the lower-specificity branch inherits the group's higher score and can flip a close cascade 4.x resolved per-row. The nesting-collapse grouping is the flattened-output form (collapseNesting: true); the 5.x-default nested output keeps the multi-parent header a plain comma list, so this applies to flattened output. Extend's :is() grafting appears in both nested and flattened output.