inStyle (Modifying the Current Selector `&` in Sass)

Avatar of Filip Naumovic
Filip Naumovic on (Updated on )

The following is a guest post by Filip Naumovic from Salsita Software. Filip has built a Sass tool to help with an issue I know I’ve experienced many times. You’re happily nesting in Sass. You’re maybe a level or two deep, and you need to style a variation based on some parent selector. You need to either break out of the nesting and start a new nesting context, or go nuclear with @at-root. I’ll let Filip tell the story of his new tool that changes that.

Sass has helped a lot of us tremendously with cleaning up our CSS code bases. When it arrived on the scene, some extremely useful patterns emerged and got quickly adopted.

For example, the usage of & and nesting allowed for the cleaning up of otherwise rather gross code blocks.

This:

.my-app { display: block; }
.my-app .widget { border-radius: 5px; }
.my-app .widget.blue { color: blue; }
.isIE6 .my-app .widget { background-image: url('fake-borders.png'); }
@media (max-width: 768px) { .my-app .widget { float: left; } }

Turned into this:

.my-app {
  display: block;
  .widget {
    border-radius: 5px;
    &.blue {
      color: blue;
    }
    .isIE6 & {
      background-image: url("fake-borders.png");
    }
    @media (max-width: 768px) {
      float: left;
    }
  }
}

What a positive change for our sanity!

All style variations of the .widget element are clearly nested below itself, using indentation as both a visual cue for relevancy and a query generator.

The current selector (&), in this case, provides a shortcut for the most common patterns. Styling the variation of an element that is invoked either by a property of the element itself or a prepending parent state.

Nested media queries are a younger addition, but they hint that evolution towards indented syntax for styles comes almost naturally. It’s easy to read and navigate, because it somehow mirrors the familiar DOM structure and keeps all styles for an element in one place, while still producing our precious, yet sometimes complicated, selectors.

Today, nesting and current selector features are present in Less, Sass, and Stylus. With a bit of wine, one could almost call it a standard.

A classic case of “You can’t do that.”

Using the above code block as an example, let’s add styles for .my-app.expanded .widget.

Despite our mighty tools, we quickly find ourselves with limited choices:

Option 1

Using the modern @at-root directive (or / in Stylus), we leave the current scope entirely and repeat the full root query to keep the relevant new styles nested below .widget, because the current selector can’t help us express this relationship.

.my-app {
  display: block;
  .widget {
    border-radius: 5px;
    &.blue {
      color: blue;
    }
    .isIE6 & {
      background-image: url("fake-borders.png");
    }
    // repeating the root selector here
    @at-root .my-app.expanded .widget {
      color: red'
    }
    @media (max-width: 768px) {
      float: left;
    }
  }
}

This creates harder to read code with a lot of duplicity, especially when real world usage extends way over our small example piece. But, it keeps our glorious nesting paradigm intact.

Option 2

We create a new code block below .my-app and use it to change all child elements relevant to the .expanded state. This means that our .widget is now styled in different places, and this separation grows for every added state in each element in the nest.

.my-app {
  display: block;
  .widget {
    border-radius: 5px;
    &.blue {
      color: blue;
    }
    .isIE6 & {
      background-image: url("fake-borders.png");
    }
    @media (max-width: 768px) {
      float: left;
    }
  }
  &.expanded .widget
     color: red;
  }
}

While it is in direct violation of our “nesting all relevant styles” dream, it’s the imperfection we learned to live with. Many of you would probably even defend this pattern, because it has been the way things are done for quite a while.

However, for the sake of choice, wouldn’t it be great to have an Option 3? One that would allow us to express the simple change in .my-app.expanded that influences our .widget without having to escape the context?

This idea has been secretly bothering me for quite a while, if only out of some form of OCD about my own stylesheets. I’ve made it my sidequest to try and find this missing tool in the style shed.

Finding Option 3

While digging around the topic, I’ve found spider webs, eternal discussions, and wildly varying propositions, many of which suggested adding some special syntax to the current selector character &. Doing that would mean months of learning complicated core libraries and fighting the long war, which instantly felt like an unacceptable burden.

Secondly, I think & works well because it’s a clear representation of the whole context, and for that reason it might be problematic adding more features to it. It does one thing and does it well, so creating a good partner to it seemed like a better idea at this time.

For sake of easy integration, I’ve decided to implement the idea on the level of preprocessor language, so you could just @import and use it right away. Preprocessors are powerful frameworks nowadays, so why not?

My first choice was Stylus, because it’s just so awesome. Unfortunately, due to issue 1703, the current selector placeholder cannot be modified inside a mixin function as of right now. Like a good zealot I’ll wait until the end of time for Stylus to fix it, but I had to keep searching for something I could implement now.

You shall not parse the current selector, as I’ve learned, in Less, so that was out.

SassScript on the other hand proved to be a powerhouse. While it is missing many useful abstractions for manipulation with strings and arrays, it is very possible to craft such functions manually. Many of them are already provided by Sass Prince Kitty Giraudel.

After months of controlled string terror…

inStyle for Sass 3.4+ is born!

Cheesy name, I know. But it’s suggestive of the functionality, because you want this thing readable in the actual code. Mixin syntax is already familiar with preprocessor users, so having a suggestive name for describing changes in element parents sounded right to me as an added bumper against unfamiliarity.

Either way, all of it has to stay readable while handling complex cases, otherwise it loses purpose in favor of @at-root selector approaches or just nesting the code elsewhere. I decided to go with two basic mechanisms that I believe address even the most despicable needs, while keeping a logically simple parsing algorithm:

Use 1) Modification

Additions to a compound element present in the current selector proved to handle ~80% of real world code, just like our first example tries to achieve.

.my-app {
  display: block;
  .widget {
    border-radius: 5px;
    &.blue {
      color: blue;
    }
    .isIE6 & {
      background-image: url("fake-borders.png");
    }
    @include in(".my-app.expanded") {
      color: red; // .my-app.expanded .widget { };
    }
    @media (max-width: 768px) {
      float: left;
    }
  }
}

Try to read that like this:

styling .widget in the .my-app.expanded state.

The function searches the nest bottom to top for the first occurrence of .my-app element (skipping current element) and appends the class .expanded to it, returning a new selector.

What about longer queries and combo modifications?

table {
  table-layout: fixed;
  thead {
    font-weight: normal;
    tr {
      height: 30px;
      td {
        background-color: #fafafa;
        &.user {
          font-weight: bold'
        }
        @include in('table.one tr.two:hover') {
          background-image: url(rainbow.png) // table.one thead tr.two:hover td { };
        }
      }
    }
  }
}

The tr parent is found and modified with .two:hover. Going upwards, table is also found and modified with .one, other elements are skipped.

Irrelevant multi-selectors are removed from the new selector:

ul, ol {
  list-style: none;
  li {
    display: inline-block;
    a {
      text-decoration: underline;
      @include in("ol.links") {
        color: orange; // ol.links li a { };
      }
    }
  }
}

Impossible cases and invalid CSS queries produce a blocking Sass error on compilation:

table {
  table-layout: fixed;
  td {
    height: 30px;
    @include in("table^s&()#") {
      what: the; // ERROR, invalid CSS
    }
    @include in ("tr.green:hover") {
      border-color: green; // ERROR, no tr or tr.green to modify in &
    }
  }
}

While crash testing this in production (hah!), I found another very practical need that I couldn’t satisfy with modifications of the parent tree only. In fact, it solves the example above, because you have to be able to do just that with tr.green:hover. You just have to be able to say where.

Use 2) Insertion

Let’s assume the following:

table {
  table-layout: fixed;
  thead {
    font-weight: normal;
  }
  tr {
    height: 30px;
  }
  td {
    background-color: #fafafa;
  }
}

Where would you ideally nest a table thead tr selector? By the dogma, you seemingly have to add it as follows:

table {
  table-layout: fixed;
  thead {
    font-weight: normal;
    tr {
      height: 50px;
    }
  tr {
    height: 30px;
  }
  td {
    background-color: #fafafa;
  }
}

However, the styled element in question is tr and you already have that as a generic style, so theoretically, nesting it below itself as a variant might be closer to how you think about the relationship, filling the gaps that current selector & cannot describe.

In this case, it means there has to be a simple way to insert some selector at a certain position above the current element while also allowing combinations with compound modifications. I couldn’t imagine this without adding a special character, so I went with the visually suggestive ^ caret.

table {
  table-layout: fixed;
  thead {
    font-weight: normal;
  }
  tr {
    height: 30px;
    @include in("^thead") {
      height: 50px; // table thead tr { };
    }
  }
  td {
    background-color: #fafafa;
    @include in("table.blue-skin ^tbody") {
      background-color: blue; // table.blue-skin tbody td { };
    }
  }
}

In this case, the caret is inserting thead one level above current or last modified element. More carets mean higher jumps in the current selector:

main {
  display: flex;
  > div {
    flex-grow: 1;
    span {
      display: inline-block;
      &.highlight {
        outline-style: dashed;
        @include in("^^.section.active") {
          outline-style: solid; // main .section.active > div span.highlight { };
        }
        @include in("^^^.section") {
          some: thing; // ERROR, inserting too high, it would equal to ".section &"
        }
      }
    }
  }
}

Note: &.highlight is the same element as span, so the insertion treats it as one step in the nest

I think inStyle shines in the simplest cases, which are also by far the most common. But things can get more complex if needed.

Use 3) Advanced combinations

The matching algorithm allows you to go even wilder with inserting in or modifying more compounds at once:

.my-app {
  display: flex;
  section {
    flex: 1;    
    header {
      width: 100%;
      @include in("^^.wrapper ^.dialog)") {
        height: 50px; // .my-app .wrapper section .dialog header { };
      }
      @include in("^.overlay ^.sidebar") {
        position: fixed; // .my-app section .overlay .sidebar header { };
      }
      @include in("^.modifier section.next ^.parent") {
        opacity: 0; // .my-app .modifier section.next .parent header { };
      }
    }
  }
}
  1. .dialog is inserted one level above header and .wrapper is inserted two levels.
  2. .sidebar is inserted above header and .overlay directly above it.
  3. Pushes .parent above header, modifies section with .next and then pushes .modifier above it.

This reminds me, perhaps you have some feedback! I’ve been thinking about enabling some simpler syntax when you want to insert more compound elements directly after each other as in the second case, perhaps something like @include in("^(.overlay .sidebar)") or improve the parser and enable more natural @include in("^.overlay .sidebar"). Let me know your opinion!

After using it for a while, I’ve found that most of my inconvenient code patterns are solved rather easily by just changing one element here and there or pushing a new selector at a certain position and keep things in place. Still, I need to be honest, it is potentially quite invasive to your usual code organization by nature of the idea.

I can see how using inStyle might bring on heated arguments. My colleagues seem to be either open minded or don’t care, which is both great.

If you use it, I would hope that the correct handling would be like with any other tool: when it’s fit for the job. Spamming complex nested mixins will unlikely score high on readability than flat out writing the full query, but on the other hand it can simplify most real world problems while keeping a slim footprint.

In the near future, I’d like to get the Stylus port working and perhaps create an Atom editor plugin to display the resulting query as a hint in the code.

It was fun taking a shot at solving the first-world problems of CSS and it is my hope that you consider the subject at least worthy of a discussion. The project is open source, so feel free to get onboard with either code or feedback!

Love it or hate it, here it is on GitHub, here’s a little microsite and here’s a live debugger for good measure.

See the Pen inStyle Crash Test Dummy by Salsita Software (@salsita) on CodePen.

Thanks for reading!