Skip to content

Instantly share code, notes, and snippets.

@apfelbox
Created March 11, 2015 10:56
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save apfelbox/26605e0d3d2adafd55d9 to your computer and use it in GitHub Desktop.
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" -- 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.

The simple implementation

@mixin double-ampersand {
	& + & {
		@content;
	}
}

Usage is like this:

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:

.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;
}

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, ...

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment