The double ampersand -- or as A list apart called it "lobotomized owl selector" -- is a CSS rule that looks like the following:
* + * {
/* some declarations */
}
You can also use it with a specific selector:
p + p {
margin-top: 1.5rem;
}
This article now looks at implementing this functionality in Sass using a mixin.
@mixin double-ampersand {
& + & {
@content;
}
}
Usage is like this:
p {
@include double-ampersand {
margin-top: 1.5rem;
}
}
The issue with this mixin will be apparent as soon as you use nested selectors. In a nested selector the CSS should look like this:
.content p + p {
margin-top: 1.5rem;
}
If you now go to your Sass file and write:
.content p {
@include double-ampersand {
margin-top: 1.5rem;
}
}
it will not produce the desired output. Instead it will produce
.content p + .content p {
margin-top: 1.5rem;
}
The improved mixin will only duplicate the last selector. So let's first look at the desired output and afterwards discuss the implementation
It should support simple selectors, ...
p {
@include better-double-ampersand {
margin-top: 1.5rem;
}
}
// ... should produce ...
p + p {
margin-top: 1.5rem;
}
... multiple selectors, ...
a,
p {
@include better-double-ampersand {
margin-top: 1.5rem;
}
}
// ... should produce ...
a + a,
p + p {
margin-top: 1.5rem;
}
... nested selectors, ...
.content p {
@include better-double-ampersand {
margin-top: 1.5rem;
}
}
// ... should produce ...
.content p + p {
margin-top: 1.5rem;
}
... multiple nested selectors, ...
.test p,
.content p {
@include better-double-ampersand {
margin-top: 1.5rem;
}
}
// ... should produce ...
.test p + p,
.content p + p {
margin-top: 1.5rem;
}
... and multiple nested selectors with different last selectors.
.test p,
.content a {
@include better-double-ampersand {
margin-top: 1.5rem;
}
}
// ... should produce ...
.test p + p,
.content a + a {
margin-top: 1.5rem;
}
So, here goes. This is the commented code that solves all requests (it is a bit verbose, sorry about that).
It tries to use the existing &
behaviour and only breaks out of it, if it would produce invalid selectors.
// This function will return the last item in a list
@function last ($list) {
@return nth($list, length($list));
}
// This function implements the improved double ampersand algoithm
@mixin better-double-ampersand {
// at first we need to reference the list of selectors for which
// this mixin is called (we will call that "caller selectors" from now on)
//
// For
// p .test, a { @include better-double-ampersand { /* ... */ }; }
// this will be
// (p .test, a)
$caller-selectors: &;
// We need to track whether the last selector for all caller selectors
// is the same. If it isn't we need to perform some special handling
$has-same-last-caller-selector: true;
// For checking whether all last selectors are the same. Store the first one
// and compare all other last selectors to it.
$previous-last-selector: last(nth($caller-selectors, 1));
// A list of prepared separators. If the last selectors are not the same,
// we need to create our own block with the prepared selectors.
$prepared-selectors: ();
// Loop through all caller selectors to
// - check for the last
// - generate prepared selectors
@each $selector in $caller-selectors {
$last: last($selector);
@if ($previous-last-selector != $last) {
$has-same-last-caller-selector: false;
}
// generate prepared selector
$prepared-selectors: append($prepared-selectors, #{$selector} + #{$last}, comma);
}
@if ($has-same-last-caller-selector) {
// if all selectors have the same last selector
// we can just use the regular `&` functionality
& + #{$previous-last-selector} {
@content;
}
} @else {
// If not all selectors are the same, we need to render a completely
// own block
@at-root #{$prepared-selectors} {
@content;
}
}
}
Please note that this code currently doesn't work in libsass. :(
You can inline the last()
function, if you want to keep it all inside of one mixin.