Sass @at-root

September 13th, 2015 - Bonn

Last week we’ve worked on defining the typography section for the style reference of our app, and I found a case that I couldn’t solve with the sass ampersand selector, at least not the way I wanted too, until I read what you can do with @at-root.

We wanted to define our own text emphasis variants (text-primary, text-success, … etc.), similar to those from bootstrap, but without applying a darker color to links when hovering or focusing on them.

I took a look at boostrap’s implementation:

/* less */

.text-emphasis-variant(@color) {
  color: @color;
  a&:hover,
  a&:focus {
    color: darken(@color, 10%);
  }
}

.text-primary{
  .text-emphasis-variant(blue);
}
    
/* css */

.text-primary {
  color: blue;
}
a.text-primary:hover,
a.text-primary:focus {
  color: #0000cc;
}
    

I figured that the I could use sass ‘&’ in the same way, after all, it lets you do things like these:

/* sass */

foo{
  &.bar{
    color: blue;
  }
  &bar{
    color:blue;
  }
  & bar{
    color:blue;
  }   
  bar &{
    color:blue;
  }
}
    
/* css */

foo.bar {
  color: blue;
}

foobar {
  color: blue;
}

foo bar {
  color: blue;
}

bar foo {
  color: blue;
}
    

Seems that something like a.& {...} would work nicely, unfortunately that’s not valid syntax.

Next thing to try was interpolation, sometimes it does the trick, for example when dealing with variables in calc

/* sass */

$width: 20px;

#foo{
  width: calc(100% - $width);
}

#bar{
  width: calc(100% - #{$width});
}
    
/* css */

#foo {
  width: calc(100% - $width);
}

#bar {
  width: calc(100% - 20px);
}
    

In our case the result is still not the intended:

/* sass */

.text-primary{
  a#{&}{
    color: blue;
  }
}
    
/* css */

.text-success a.text-success {
  color: blue;
}
    

Here is when we can make us of the @at-root directive, which will jump out of your current position to the top-level of the document

/* sass */
.text-primary{
  @at-root {
    a#{&} {
      color: blue;
    }
  }
}

    
/* css */
a.text-primary {
  color: blue;
}
    

This let us rewrite the less implementation of .text-emphasis-variant like this:

/* sass */
@mixin text-emphasis-variant($color) {
  color: $color;
  @at-root {
    a#{&}:hover, a#{&}:focus {
      color: $color;
    }
  }
}

.text-primary {
  @include text-emphasis-variant(blue);
}

    
/* css */
.text-primary {
  color: blue;
}
a.text-primary:hover, a.text-primary:focus {
  color: blue;
}

    

Now that Bootstrap 4 is moving from less to sass, I checked out how did they go around to solve it:

/* https://github.com/twbs/bootstrap/blob/v4-dev/scss/mixins/_text-emphasis.scss */

@mixin text-emphasis-variant($parent, $color) {
  #{$parent} {
    color: $color;
  }
  a#{$parent} {
    @include hover-focus {
      color: darken($color, 10%);
    }
  }
}

/* https://github.com/twbs/bootstrap/blob/v4-dev/scss/_utilities.scss */

@include text-emphasis-variant('.text-primary', $brand-primary);

It’s a very nice solution, if you can reference the parent element directly, it might be even easier to read. If you want to jump out of more levels or use the mixin inside your class definition, then you have @at-root available.

Another case of use would be to apply it to keep related declarations together that belong to a single component. In this example from using sass 33s at root for piece of mind, we have a class with a given animation written out of it’s context:

.avatar {
  background-color: red;
  height: 120px;
  margin: 40px;
  width: 120px;
  &:hover {
    animation: sizeme .8s infinite ease-in alternate;
  }
}
@keyframes fade {
  0% { transform: scale(1.0); }
  25% { transform: scale(1.1); }
  50% { transform: scale(1.0); }
  75% { transform: scale(1.2); }
  100% { transform: scale(1.1); }
}

Instead, we can write it like this:

.avatar {
  background-color: red;
  height: 120px;
  margin: 40px;
  width: 120px;

  @at-root {
    @keyframes fade {
      0% { transform: scale(1.0); }
      25% { transform: scale(1.1); }
      50% { transform: scale(1.0); }
      75% { transform: scale(1.2); }
      100% { transform: scale(1.1); }
    }
  }
  &:hover {
    animation: fade .8s infinite ease-in alternate;
  }
}