Advanced: extend
extend is the mirror image of a mixin. A mixin copies a
block's declarations into your selector; extend does the opposite — it copies
your selector up to another rule, so both share one block of CSS instead of
duplicating it. It's the leanest way to say "style me like that thing, too."
The basic idea
Attach &:extend(<target>) inside a rule (or the Jess $extend <target>;
statement), and your selector is added wherever the target already appears.
- Jess
- Less
- CSS output
.animal { background: black; color: white; }
.bear {
$extend .animal; // style .bear like .animal
background: brown;
}
.animal { background: black; color: white; }
.bear {
&:extend(.animal); // style .bear like .animal
background: brown;
}
.animal,
.bear {
background: black;
color: white;
}
.bear {
background: brown;
}
The extend clause itself is never emitted — it's resolved away, leaving your
rule's own declarations behind. Matching runs against the compiled CSS (after
nesting is resolved), not the source text.
Forms
- Jess
- Less
// statement form (Jess-native)
.a { $extend .b; }
// multiple targets
.a { $extend .b, .c; }
// attached to the selector — clause must be last
.a:extend(.b) {}
// inside the body — shorthand for attaching to every selector
.a { &:extend(.b); }
// multiple targets
.a:extend(.b, .c) {}
In Less, the body form .a { &:extend(.b); } is exactly equivalent to attaching
:extend(.b) to every selector of the rule.
Matching mode
Jess $extend .b; uses partial matching by default. It matches .b wherever
that selector appears, then rewrites that spot in place.
- Jess
- Less
- CSS output
.clearfix {
&:after { content: ''; clear: both; }
}
.foo { $extend .clearfix; color: red; } // partial (all) by default
.clearfix {
&:after { content: ''; clear: both; }
}
.foo { &:extend(.clearfix all); color: red; }
:is(.clearfix, .foo):after {
content: '';
clear: both;
}
.foo {
color: red;
}
Because .clearfix is matched inside the compound .clearfix:after, the match
is grafted in place as :is(.clearfix, .foo):after.
:is() output
When several selectors extend into the same spot, Jess combines them into a
single :is(…) group rather than duplicating the whole rule once per extender.
This keeps the output compact:
- Jess
- CSS output
.clearfix { &:after { content: ''; } }
.foo { $extend .clearfix; }
.bar { $extend .clearfix; }
:is(.clearfix, .foo, .bar):after {
content: '';
}
When the target is the whole selector (not part of a compound), the extenders
simply join the selector list — no :is() needed:
.clearfix,
.foo,
.bar {
/* … */
}
Chained extends
Extends resolve to a fixpoint: if .b extends .a and .c extends .b, then
.c chains all the way through. Ordering doesn't matter — the extender may come
before the target.
- Jess
- CSS output
.a { color: black; }
.b { $extend .a; }
.c { $extend .b; }
.a,
.b,
.c {
color: black;
}
Self-references and circular references are handled safely — they simply resolve and stop, never loop.
Exact matching with !exact
Most extends do not need !exact. Use it when the target must be the whole
selector, not a piece inside a larger selector.
For $extend .animal;, these targets match differently:
| Target rule | Default $extend .animal; | $extend .animal !exact; |
|---|---|---|
.animal | matches | matches |
.animal:hover | matches .animal inside the compound | no match |
.card .animal | matches .animal at the end | no match |
.animal.large | matches .animal inside the compound | no match |
!exact can still target a complex selector. In this example, .bear only joins
the rule whose whole selector is .card .animal:
.card .animal { color: black; }
.panel .card .animal { color: gray; }
.card .animal:hover { color: brown; }
.bear { $extend .card .animal !exact; }
.card .animal,
.bear {
color: black;
}
.panel .card .animal {
color: gray;
}
.card .animal:hover {
color: brown;
}
Without !exact, the same target would also match the embedded
.card .animal in the descendant and hover selectors.
Nested selectors
Extend matches nested selectors too, because it works on the compiled result:
- Jess
- CSS output
.sidebar {
width: 300px;
.box { border: 1px solid; }
}
.sidebar2 { $extend .sidebar; background: blue; }
.sidebar,
.sidebar2 {
width: 300px;
}
:is(.sidebar, .sidebar2) .box {
border: 1px solid;
}
.sidebar2 {
background: blue;
}
Extending inside @media
An extend written inside a @media block only matches selectors in that same
media (or a nested one) — it won't reach the top level or a sibling media. A
top-level extend reaches everything, including inside media blocks.
- Jess
- CSS output
.ext1 .outside { color: inherit; } // outside media — not matched from inside
@media (tv) {
.ext1 .inside { color: black; }
.tv-only {
$extend .ext1; // matches only within this media
background: blue;
}
}
.ext1 .outside {
color: inherit;
}
@media (tv) {
:is(.ext1, .tv-only) .inside {
color: black;
}
.tv-only {
background: blue;
}
}
Nested @media blocks stay nested in Jess's output — they are not merged.
Reference-mode visibility
A @compose (reference) / @import (reference) module hides its own rules from the
output. If your extender targets a selector that lives in a referenced module, only
your selector surfaces — carrying the referenced rule's declarations — while the
referenced target header itself is never emitted. Extend is the sanctioned way to pull
styles out of a reference-imported sheet.
What extend does not do
- It is selector-level, not property-level. Extend shares a rule's whole declaration block. It cannot cherry-pick a single property — reach for a mixin or a variable for that.
- It does not match variable targets. An interpolated selector used as a match target matches nothing. (An interpolated selector as the extender is fine.)
- It does not normalize the target form. Matching is exact: a leading star, the
order of pseudo-classes, and the
nthform all matter. Only attribute-selector quote type is normalized.
When to reach for extend
- Avoid a base class. Style
.bearlike.animalwithout addingclass="animal bear"to your markup. - Shrink your CSS. A mixin copies declarations into every caller;
extendmoves the selectors up to one shared block, so the declarations appear once. - Relate two unrelated blocks. Mixins only take simple selectors;
extendcan graft complex selectors together (button.list-stylesharingli.list > a's styles).
The full behavior — exact-match strictness, the fixpoint termination rules,
:is() grafting, and @media scoping — is documented for contributors in
docs/architecture/core/EXTEND-SEMANTICS.md.