82

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.

BBaysinger
  • 6,614
  • 13
  • 63
  • 132
Zeljko
  • 5,048
  • 5
  • 36
  • 46
  • 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 Feb 17 '12 at 07:00
  • 2
    It doesn't escape from real-life too much: what I wanted is to have different layout if message-error is either

    or

      . Example: for

      element, box would have some background-url image at left side (check Constellation admin template). But if it is

        , no sprites.
    – Zeljko Feb 17 '12 at 18:20
  • 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 Feb 19 '12 at 22:05
  • Have you got any solution for this ? – Madan Bhandari Apr 17 '16 at 18:26
  • @cimmanon's answer should be selected as the solution, since there is now support for this feature. – BBaysinger Jul 28 '21 at 04:21

13 Answers13

74

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.

cimmanon
  • 67,211
  • 17
  • 165
  • 171
  • 3
    @at-root is a cool new feature, but it fails miserably when nested within another level of selectors. Though it might work in Zeljko's case. http://sassmeister.com/gist/bf8492e469d91740194b – Adam Youngers Dec 02 '14 at 19:47
  • 1
    @at-root suffers the same problem that .me {p & {}} suffers in that it jumps all the way up to the root. In my case I use page specific wrapping classes to namespace the css where using either of these will fail. – Adam Youngers Dec 02 '14 at 19:58
35

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;
    }
  }
}
Dominic
  • 62,658
  • 20
  • 139
  • 163
  • 5
    Look at the screenshot. `.Parent-Child` is not a realistic/extensible scenario. `.parent .child` or `.parent > .child` is - and you can see what happens in the screenshot – Tobi Akinyemi Feb 14 '21 at 22:09
  • 1
    Sorry for the bump, but where is any documentation on this? It's not working for me (and not in online SCSS compilers) – Janis Jansen Nov 22 '21 at 15:44
24

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.

piouPiouM
  • 4,937
  • 1
  • 21
  • 22
  • I found that one too. In fact, it was supported in older version and is supported by Stylus (which I would use but don't know how to install). That's why I was looking for some patch, hack or so. – Zeljko Feb 21 '12 at 00:48
  • 1
    considering the answer by @cinnamon 2 years later it's really a disappointment that the original creator used "never" do describe the implementation of this feature. Lesson learned for them I guess. – Jacksonkr Mar 27 '20 at 18:52
  • It should be noted in this answer that there is now a supported method. – BBaysinger Jul 28 '21 at 04:18
12

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.

pyronaur
  • 3,515
  • 6
  • 35
  • 52
  • 1
    I would say that unless you have very few classes to do this to, I would refrain from using @extend. You can end up with HUGE CSS files as this creates `.message-error, p.message-error, ... { ... }`. One or two is fine, just use wisely. – Alex McCabe Jul 29 '16 at 11:16
5

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.

Quentin Veron
  • 3,079
  • 1
  • 14
  • 32
3

@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
     }
  }
Tian
  • 662
  • 2
  • 8
  • 23
  • 3
    Adding a space will make the output `p .message-error`, I think the OP is looking for `p.message-error` (no space); while your suggestion works, I don't think it's a solution in this particular case. – mhulse Oct 05 '13 at 23:32
2

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;
}
imkremen
  • 84
  • 4
  • I (very much) like the idea behind this mixin, but I don't quite like the approach of counting the number of elements to insert the additional element: if you for any reason edit the code and add a new layer at some point, you could break your CSS badly, and finding the reason behind it could take a while... I am bookmarking your solution anyway, thank you for sharing. – Marcos Buarque Sep 06 '22 at 15:53
2

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;}
ScottS
  • 71,703
  • 13
  • 126
  • 146
1

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.

Darex1991
  • 855
  • 1
  • 10
  • 24
0

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

Adam Youngers
  • 6,421
  • 7
  • 38
  • 50
0

I use an @mixin function like this, when i need change some element in middle of a sass big tree.

The first parameters is the parent element, the target, and the second the class that should have.

SASS

@mixin parentClass($parentTarget, $aditionalCLass) {

    @at-root #{selector-replace(&, $parentTarget, $parentTarget + $aditionalCLass)} {
        @content;
    }
}
Sample,

like i need to improve font size in a strong tag, when .txt-target had .txt-strong too

HTML

   <section class="sample">
        <h1 class="txt-target txt-bold">Sample<strong>Bold</strong>Text</h1>
    </section>

SASS

section{
    .txt-target{
        strong{
            @include parentClass('.txt-target','.txt-bold'){
                font-weight:bold;
                font-size:30px;
            }
        }
    }
}

Font:

https://sass-lang.com/documentation/at-rules/at-root

Here you can see a function called @mixin unify-parent($child) that looks like this

erickocrs
  • 19
  • 5
-1

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.

wheresrhys
  • 22,558
  • 19
  • 94
  • 162
  • I had thought about something like this too, but I believe the output of the above (assuming the interpolation works as expected) would actually be `.message-error p.message-error {...}` whereas with the original attempted use by Zeljko of the parent selector `&` (had it worked like he wanted) it would not nest, and produce `p.message-error {...}`. I don't know this for sure, as I am not a SASS expert, but from reading the documentation, it seems I've understood correctly what the output would be for your solution. – ScottS Feb 23 '12 at 10:23
  • oh yeah... well spotted. I've edited to something else that might work, though less tidy – wheresrhys Feb 23 '12 at 10:34
  • Thanks guys, but this looks worse than p.message-error{} under .message-error{}. And it is still not nested as I need. I think I will either learn Stylus or create a ticket for SASS. – Zeljko Feb 23 '12 at 18:49
  • So two things here. Correct me if I am wrong, but I don't believe the brackets wrapping without a selector will work. Also you need to place quotes around your class name. – Adam Youngers Dec 02 '14 at 19:22
-3

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