65
votes

I am using SASS and found an inconvenience. This is an example of what I am trying to do:

.message-error {
    background-color: red;

    p& {
        background-color: yellow
     }
  }

Expected CSS:

.message-error {
    background-color: red;
}
p.message-error {
    background-color: yellow ;
}

The idea: all elements with .message-error will be red, except if it is p.message-error. This is not real-life situation, just to show an example.

SASS is not able to compile this, I even tried string concatenation. Is there some plugin that will do exactly the same?

NOTE: I know I can put another CSS definition like:

p.message-error{....}

...under, but I would like to avoid that and use one place for all .message-error definitions.

Thanks.

12
Like you said: this is not a real-life situation. Whether you're using CSS or Sass, it's better to do what you know to do. There's no reason to do what you're proposing. If there is, please clarify.maxbeatty
It doesn't escape from real-life too much: what I wanted is to have different layout if message-error is either <p> or <ul>. Example: for <p> element, box would have some background-url image at left side (check Constellation admin template). But if it is <ul>, no sprites.Zeljko
Don't know about sass. But in normal CSS if u want "all elements with .message-error will be red, expect if it is p.message-error" from which I assume that the p.message-error does not need to be yellow either, than we could use the negation pseudo-class selector ".message-error:not(p.message-error)" - Not sure about this.Jawad
Have you got any solution for this ?Madan Bhandari
@cimmanon's answer should be selected as the solution, since there is now support for this feature.BBaysinger

12 Answers

59
votes

As of Sass 3.4, this is now supported. The syntax looks like this:

.message-error {
    background-color: red;

    @at-root p#{&} {
        background-color: yellow
    }
}

Note the @at-root directive and the interpolation syntax on the ampersand. Failure to include the @at-root directive will result in a selector like .message-error p.message-error rather than p.message-error.

23
votes

You can assign the current selector to a variable and then use it at any depth:

.Parent {
  $p: &;

  &-Child {
    #{$p}:focus & {
      border: 1px solid red;
    }

    #{$p}--disabled & {
      background-color: grey;
    }
  }
}
22
votes

Natalie Weizenbaum (the lead designer and developer of Sass) says it will never be supported:

Currently, & is syntactically the same as an element selector, so it can't appear alongside one. I think this helps clarify where it can be used; for example, foo&bar would never be a valid selector (or would perhaps be equivalent to foo& bar or foo &bar). I don't think this use case is strong enough to warrant changing that.

Source: #282 – Element.parent selector

To my knowledge, there is no possible workaround.

11
votes

The best thing to do would be probably this (assuming you have a little more in your .message-error class than just background color.

.message-error {
  background-color: red;
}

p.message-error {
  @extend .message-error;
  background-color: yellow
}

This approach doesn't offer that close grouping, but you can just keep them close to each other.

5
votes

I had the same problem so I made a mixin for that.

@mixin tag($tag) {
  $ampersand: & + '';
  $selectors: simple-selectors(str-replace($ampersand, ' ', ''));

  $main-selector: nth($selectors, -1);
  $previous-selectors: str-replace($ampersand, $main-selector, '');

  @at-root {
     #{$previous-selectors}#{$tag}#{$main-selector} {
      @content;
    }
  }
}

To make it work, you will need a string replacement function as well (from Hugo Giraudel):

@function str-replace($string, $search, $replace: '') {
  $index: str-index($string, $search);
  @if $index {
    @return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace);
  }
  @return $string;
}

How it works:

SCSS

.foo {
  color: blue;

  @include tag(p) {
    color: red;
  }
}

Output

.foo {
  color: blue;
}

p.foo {
  color: red;
}

Use case
This method works with nested selectors but not whit compound ones.

2
votes

I think if you want to keep them grouped by parent selector, you might need to add a common parent:

body {
    & .message-error {background-color: red;}
    & p.message-error {background-color: yellow}
}

Of course, body could be replaced with some other common parent, such as #Content or another div name that will contain all the error messages.

UPDATE (Another Idea)

If you leverage @for and lists then it seems like this should work (what I don't know for sure is if the list will allow the . (period).

@for $i from 1 to 3 {
  nth(. p. ul., #{$i})message-error {
    background-color: nth(red yellow cyan, #{$i}));
  }
}

Should compile to something like:

.message-error {
   background-color: red;}
p.message-error {
   background-color: yellow;}
ul.message-error {
   background-color: cyan;}
2
votes

@Zeljko It is no possible to do what you want via SASS.

See Nex3 comment: https://github.com/nex3/sass/issues/286#issuecomment-7496412

The key is the space before the '&':

.message-error {
    background-color: red;

    p & {
        background-color: yellow
     }
  }

instead of:

.message-error {
    background-color: red;

    p& {
        background-color: yellow
     }
  }
2
votes

I made a mixin that solves this problem.

Github: https://github.com/imkremen/sass-parent-append

Example: https://codepen.io/imkremen/pen/RMVBvq


Usage (scss):

.ancestor {
  display: inline-flex;

  .grandparent {
    padding: 32px;
    background-color: lightgreen;

    .parent {
      padding: 32px;
      background-color: blue;

      .elem {
        padding: 16px;
        background-color: white;

        @include parent-append(":focus", 3) {
          box-shadow: inset 0 0 0 8px aqua;
        }

        @include parent-append(":hover") {
          background-color: fuchsia;
        }

        @include parent-append("p", 0, true) {
          background-color: green;
        }
      }
    }
  }
}

Result (css):

.ancestor {
  display: inline-flex;
}
.ancestor .grandparent {
  padding: 32px;
  background-color: lightgreen;
}
.ancestor .grandparent .parent {
  padding: 32px;
  background-color: blue;
}
.ancestor .grandparent .parent .elem {
  padding: 16px;
  background-color: white;
}
.ancestor:focus .grandparent .parent .elem {
  box-shadow: inset 0 0 0 8px aqua;
}
.ancestor .grandparent .parent:hover .elem {
  background-color: fuchsia;
}
.ancestor .grandparent .parent p.elem {
  background-color: green;
}
1
votes

I created package/mixin with a similar solution :) (Maybe it will help U)

https://github.com/Darex1991/BEM-parent-selector

so writing:

.calendar-container--theme-second-2 {
  .calendar-reservation {
    @include BEM-parent-selector('&__checkout-wrapper:not(&--modifier):before') {
      content: 'abc';
    }
  }
}

This mixin will add selector only for the last parent:

.calendar-container--theme-second-2 .calendar-reservation__checkout-wrapper:not(.calendar-reservation--modifier):before {
   content: 'abc';
 }

More info on the repo.

0
votes

I have ran into this before as well. Bootstrap 3 handles this using a parent selector hack. I've tweaked it slightly for my own purposes...

@mixin message-error() {
  $class: '.message-error';
  #{$class} {
    background-color: red;
  }
  p#{$class} {
    background-color: yellow;
  }
}
@include message-error();

wheresrhys uses a similar approach above, but with some sass errors. The code above allows you to manage it as one block and collapse it in your editor. Nesting the variable also makes it local so you can reuse $class for all instances where you need to apply this hack. See below for a working sample...

http://sassmeister.com/gist/318dce458a9eb3991b13

-1
votes

This cheat might work

 {
     $and: .message-error;
     #{$and} {
        background-color: red;
     }

     p#{$and} {
        background-color: yellow
     }
  }

You may even be able to use $& as your variable name but I'm not 100% sure it won't throw an error.

And SASS has inbuilt scoping, which removes having to worry about the value of $and leaking out to the rest of your stylesheet

Variables are only available within the level of nested selectors where they’re defined. If they’re defined outside of any nested selectors, they’re available everywhere.

-2
votes

In the Current Release: Selective Steve (3.4.14) this is now possible, just need to update a little bit your code:

.message-error {
    background-color: red;
    p &{
        background-color: yellow
     }
 }

this only works if you are one level nested, for instance it does not work if you have something like this:

.messages{
    .message-error {
        background-color: red;
        p &{
            background-color: yellow
         }
     }
 }