The @extend at-rule is probably the single most complicated feature in Sass.
While its semantics are straightforward to describe, the implementation involves
many interacting layers and a lot of intricate case analysis.
These definitions provide names to the various selectors involved with a given
use of @extend:
.extender {
@extend .target;
}
// ┌─ extendee
.target {
// ...
}An @extend rule's extender is the selector list for the style rule in
which the @extend rule appears.
An @extend rule's target is the simple selector that's used as an
argument to @extend.
An extension is a collection of various properties.
An extension is a more abstract representation of the information inherent in an
@extendrule. As such, all@extendrules define extensions, but not all extensions directly correspond to@extendrules.
- The extender, a selector list.
- The target, a simple selector.
An extendee is a selector list being modified by an extension. It's only defined within the scope of a single application of a given extension.
If an extendee contains that extensions's target, it will usually be modified to include the extension's extender as well.
As a shorthand, we use the function notation extend(extendee, target, extender) to refer to the result of extending extendee with extender {@extend target} (much like the Sass function selector-extend()). We further
use extend(extendee, extension) as a shorthand for extend(extendee, extension.target, extension.extender).
The @extend rule means that all elements matching the extender
should be styled as though they match the target as well. The
@extend rule only applies to CSS in the module in which it's defined and
that module's transitive dependencies.
Because Sass can't directly affect how the browser applies styles to elements, these semantics are approximated by duplicating each extendee with the target replaced by the extender. Rather than being a naïve textual replacement, the extender is integrated intelligently into the extendee to match the semantics as best as possible.
To execute an @extend rule rule:
-
If there is no current style rule, throw an error.
-
Let
selectorbe the result of evaluating all interpolation inrule's selector and parsing the result as a list of simple selectors. -
If
selectorcontains any parent selectors, throw an error. -
Let
extensionbe an extension whose extender is the current style rule's selector and whose target isselector. -
Add
extensionto the current module's extensions.
Note that this adds the extension to the module being evaluated, not the module in which the
@extendlexically appears. This means that@extends are effectively dynamically scoped, not lexically scoped.
This algorithm takes a module starting-module and returns a CSS tree
that includes CSS for all modules transitively used or forwarded by
starting-module.
-
Let
new-selectorsbe an empty map from style rules to selectors. For the purposes of this map, style rules are compared using reference equality, meaning that style rules at different points in the CSS tree are always considered different even if their contents are the same. -
Let
new-extensionsbe an empty map from modules to sets of extensions. -
Let
extendedbe the subgraph of the module graph containing modules that are transitively reachable fromstarting-module. -
For each module
domesticinextended, in reverse topological order:-
Let
downstreambe the set of modules inextendedwhose dependencies includedomestic. -
For each style rule
ruleindomestic's CSS:-
Let
selectorbe the result of applyingdomestic's extensions torule's selector. -
Let
selector-listsbe an empty set of selector lists. -
For each module
foreignindownstream:-
Let
extended-selectorbeextend(selector, new-extensions[foreign]).new-extensions[foreign]is guaranteed to be populated at this point becauseextendedis traversed in reverse topological order, which means thatforeign's own extensions will already have been resolved by the time we start working on modules upstream of it. -
Add
selectortoselector-lists.
-
-
Set
new-selectors[rule]to a selector that matches the union of all elements matched by selectors inselector-lists. This selector must obey the specificity laws relative to the selectors from which it was generated. For the purposes of the first law, "the original extendee" is considered only to refer to selectors that appear indomestic's CSS, not selectors that were added by other modules' extensions.Implementations are expected to trim redundant selectors from
selector-listsas much as possible. For the purposes of the first law of extend, "the original extendee" is only the selectors inrule's selector. The new complex selectors inselectorgenerated fromdomestic's extensions don't count as "original", and may be optimized away. -
For every extension
extensionwhose extender appears inrule's selector:-
For every complex selector
complexinnew-selectors[rule]:- Add a copy of
extensionwith its extender replaced bycomplextonew-extensions[domestic].
- Add a copy of
-
-
-
-
Let
cssbe an empty CSS tree. -
Define a mutating recursive procedure, traversing, which takes a module
domestic:-
If
domestichas already been traversed, do nothing. -
Otherwise, traverse every module in
domestic's dependencies.Because this traverses modules depth-first, it emits CSS in reverse topological order.
-
Let
initial-importsbe the longest initial subsequence of top-level statements indomestic's CSS tree that contains only comments and@importrules and that ends with an@importrule. -
Insert a copy of
initial-importsincssafter the last@importrule, or at the beginning ofcssif it doesn't contain any@importrules. -
For each top-level statement
statementindomestic's CSS tree afterinitial-imports:-
If
statementis an@importrule, insert a copy ofstatementincssafter the last@importrule, or at the beginning ofcssif it doesn't contain any@importrules. -
Otherwise, add a copy of
statementto the end ofcss, with any style rules' selectors replaced with the corresponding selectors innew-selectors.
-
-
-
Return
css.
It's not possible for a preprocessor to guarantee the semantics of @extend in
full generality. There are three major exceptions where implementations are not
required to meet the full definition.
-
Implementations should not try to apply native browser styles that would apply to the target. For example, while it's legal to write
@extend table, there's no good way to apply browsers' built-in table styles. -
Second, when the extender and the extendee both contain multiple compound selectors separated by combinators, implementations are allowed to assume that the elements matched by the extender's compound selectors are not interleaved with those matched by the extendee's compound selectors.
For example, consider
extend(.c .x, .x, .a .b). Implementations must generate the selectors.a .c .band.c .a .b, because an element withclass="a"may be either outside or inside one withclass="c". However, implementations are not required to generate the selector.a.c .bwhich would require HTML withclass="a c".This flexiblity is allowed because otherwise implementations would have to generate a combinatorial explosion of selectors, the vast majority of which would be extremely unlikely to match real HTML. This particular heuristic assumes that the extender and extendee were each written with self-contained HTML in mind, so that interwoven HTML is unlikely to come up.
-
Implementations are not required to apply the target's styles with the exact same specificity as the extender, because this isn't generally possible when complex extendees exist. However, implementations must respect certain guarantees about specificity; see below for details.
When modifying the extendee during extension, the implementation must provide two guarantees about the result. These are known as the "laws of extend".
The first law of @extend says that the specificity of the first generated
selector must be greater than or equal to that of the original extendee. For
example, extend(a.foo, .foo, .a) should generate a.foo, a even though
a.foo matches a subset of elements matched by a.
In most cases, the first generated selector will be identical to the extendee,
but it may need to be modified when dealing with the pseudo-selector :not().
For example, extend(:not(.foo), .foo, .bar) should produce
:not(.foo):not(.bar).
The second law of extend says that the specificity of a new selector to match a
given extender must be greater than or equal to the specificity of that
extender. For example, extend(a, a, a.foo) should produce a, a.foo even
though (again) a.foo matches a subset of elements matched by a.
This still leaves room for optimizations. For example,
extend(.bar a, a, a.foo) can just produce .bar a (omitting .bar a.foo).
This is allowed because .bar a matches a superset of the elements matched by
.bar a.foo, and the specificity of .bar a is equal to that of the extender
a.foo.