Skip to content

Instantly share code, notes, and snippets.

@apfelbox
Created March 11, 2015 10:56
Show Gist options
  • Select an option

  • Save apfelbox/26605e0d3d2adafd55d9 to your computer and use it in GitHub Desktop.

Select an option

Save apfelbox/26605e0d3d2adafd55d9 to your computer and use it in GitHub Desktop.
Improved double ampersand mixin for Sass
The double ampersand -- or as A list apart called it ["lobotomized owl selector"](http://alistapart.com/article/axiomatic-css-and-lobotomized-owls) -- is a CSS rule that looks like the following:
```css
* + * {
/* some declarations */
}
```
You can also use it with a specific selector:
```css
p + p {
margin-top: 1.5rem;
}
```
This article now looks at implementing this functionality in Sass using a mixin.
# The simple implementation
```scss
@mixin double-ampersand {
& + & {
@content;
}
}
```
Usage is like this:
```scss
p {
@include double-ampersand {
margin-top: 1.5rem;
}
}
```
## The problem
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:
```css
.content p + p {
margin-top: 1.5rem;
}
```
If you now go to your Sass file and write:
```scss
.content p {
@include double-ampersand {
margin-top: 1.5rem;
}
}
```
it will not produce the desired output. Instead it will produce
```css
.content p + .content p {
margin-top: 1.5rem;
}
```
# Improved mixin
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, ...
```scss
p {
@include better-double-ampersand {
margin-top: 1.5rem;
}
}
// ... should produce ...
p + p {
margin-top: 1.5rem;
}
```
... multiple selectors, ...
```scss
a,
p {
@include better-double-ampersand {
margin-top: 1.5rem;
}
}
// ... should produce ...
a + a,
p + p {
margin-top: 1.5rem;
}
```
... nested selectors, ...
```scss
.content p {
@include better-double-ampersand {
margin-top: 1.5rem;
}
}
// ... should produce ...
.content p + p {
margin-top: 1.5rem;
}
```
... multiple nested selectors, ...
```scss
.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.
```scss
.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.
```scss
// 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment