The Problem

Sometimes you need to control document-wide styles by adding a special class to the <body> element. A good example is a no-touch class, which selectively enables :hover styles. This can be used to remove those annoying "stuck" hover states when using touch input, while preserving hover states for mouse users. Another example is an enable-focus class used to hide :focus styles for all but keyboard users.

The toggling of such classes is usually handled by JavaScript, and proper implementations will constantly monitor for input types and toggle the classes on the <body> as needed. However, this post is just looking at the CSS side of the issue, as that is what tripped me up the most.

Sticking to the hover example, once you have your classes being set/toggled on the <body>, the challenge is to write all of your :hover selectors to take the parent class into account. Suppose you have the following CSS (SCSS example is next):

.action-bar {
  display: flex;
  align-items: center;
}

.action-bar .button {
  color: orange;
}

.action-bar .button:hover,
.action-bar .button:active {
  color: blue;
}

To account for the no-hover class and make the effect more touch-friendly, you'd rewrite the :hover selector while leaving :active alone, since that is already touch friendly.

body.no-touch .action-bar .button:hover,
.action-bar .button:active {
  color: blue;
}

This works, but you have to sprinkle the body.no-touch qualifier before all of your :hovers, which is annoying and repetative. Interestingly, the problem gets worse if you're nesting selectors with Sass. For a while this really bugged me because I've learned to love nesting selectors (in moderation, of course), and this single problem forced me to unravel my nesting and duplicate some selectors. Here's the same example written in SCSS, before applying the no-touch class:

.action-bar {
  display: flex;
  align-items: center;

  .button {
    color: orange;

    &:hover,
    &:active {
      color: blue;
    }
  }
}

I'm used to using Sass's ampersand to chain partial selectors onto a computed parent selector, but in my mind that always appends to the right side of the selector. Enter the problem: body.no-touch must be applied before the entire computed selector. That led me to write something like this:

.action-bar {
  display: flex;
  align-items: center;

  .button {
    color: orange;

    &:active {
      color: blue;
    }
  }
}

// :hover is extracted so body.no-touch can be prepended
body.no-touch .action-bar .button:hover {
  color: blue;
}

Yikes, that is horrendous. Most of the benefit of using SCSS to keep the source code maintainable has been lost.

The Solution

Through all this, a powerful feature of Sass's ampersand was sitting right in front of me not being utilized. That is why I'm writing this post, because this is a feature I just haven't seen used in a real example and I found it super handy. Turns out, you can write a selector before the & and it will be prepended! So easy and so awesome. With this knowledge, we can write the styles like so:

.action-bar {
  display: flex;
  align-items: center;

  .button {
    color: orange;

    body.no-touch &:hover,
    &:active {
      color: blue;
    }
  }
}

Very simple, yet it evaded me for far too long. Now, we're still left adding body.no-touch to all our :hover styles. Since we're in Sass-land, this pattern is just a mixin waiting to be extracted:

@mixin on-hover {
  body.no-touch &:hover,
  &:active {
    @content;
  }
}

As a bonus, we don't even have to remember to add :active selectors any longer! Cross platform hover/touch effects are now dead simple to write, and near impossible to mess up. Using the mixin, our SCSS now becomes:

.action-bar {
  display: flex;
  align-items: center;

  .button {
    color: orange;

    @include on-hover {
      color: blue;
    }
  }
}

Which compiles to the following CSS:

.action-bar {
  display: flex;
  align-items: center;
}

.action-bar .button {
  color: orange;
}

body.no-touch .action-bar .button:hover,
.action-bar .button:active {
  color: blue;
}

Which is exactly what we started off writing.

One Step Further

Another common task is to disable an element, which among other things means it shouldn't respond to input. Perhaps you set a .disabled class on some custom elements. To prevent both :hover and :active styles on our disabled elements, we can add the following to the mixin:

@mixin on-hover {
  // only apply styles if element is enabled
  &:not(.disabled) {
    body.no-touch &:hover,
    &:active {
      @content;
    }
  }
}

Now all elements which use on-hover can also be disabled without changing any CSS! 🎉