10

I need a CSS filter to apply to all elements in a container, except for specific ones. Quick example to explain the situation:

<div class="container">
 <img class="one" src="blah" />
 <img class="two" src="blah" />
 <img class="three" src="blah" />
</div>

Then I am applying filters as so:

.container {
  -webkit-filter: grayscale(100%);
  filter: grayscale(100%);
}

So the container has the greyscale filter applied to it, and all img in it are turned to grey. However, I want one of the img to not turn to grey:

.two {
 -webkit-filter: grayscale(0);
 filter: grayscale(0);
}

However, this is not working. The filter of the container seems to be overriding the filter of the contained element. Any ideas how I can get around this? Is there an easy way, or do I have to write up some jQuery to look at all the elements that aren't ".two" and apply the filter to them, rather than the container?

Update: I neglected to mention an important caveat: The container has to be greyscale, due to it having a background-image property that is to also be turned grey. This little snippet is part of more containers that are all going greyscale as well, I'm really just trying to figure out if there's a way to have an overriding exemption to the rule on the parent, since the parent has to have the rule as well.

4
  • 5
    Quite simply...NO. filter, like opacity applies to the parent and *all *children and cannot be over-ridden by setting a competing style on a child element.
    – Paulie_D
    Commented Jul 5, 2015 at 18:17
  • That's kind of what I was afraid of. Oh well! Guess this has to be done a more complicated way.
    – SpaceMouse
    Commented Jul 6, 2015 at 3:20
  • I've been scouring for answers on this as well, and it looks like what @Paulie_D says is true. The answers I see posted on here do not seem to work because if applying filter on a container, it cascades its effect down to its children. Any changes you attempt to make on the children elements are additional on top of the filter. It seems the only solution is to separate out the element(s) you don't want having a filter out of the container element and managing positioning of the elements separate of the container. Or finding an alternative to using filter itself
    – philip yoo
    Commented Jun 10, 2020 at 23:47
  • you could apply a reverse filter on the children ... this is simple if the filter applied to the parent is simple, but if multiple filters are applied to the parent things get tricky quickly - if your use case is simple this might work for you ... what we really need is someone clever with spare time to write an npm package that does all the clever calculations needed to reverse multiple filters!
    – danday74
    Commented Aug 27, 2023 at 0:05

4 Answers 4

5

According to CSS specifity rules -

Either put the .two after the .container in the css,

Or make the .two more specific, i.e. img.two

UPDATE

The .container rule is on the div itself - not on the images. So the container goes grayscale regardless of what you tell the images to do. Try changing that into .container img, and then try incorporating the answers you received.

3
  • Regardless of where I place the css for .two in the file, the filter is being applied to all of the container's elements.
    – SpaceMouse
    Commented Jul 5, 2015 at 14:35
  • sadly this answer wouldn't work - As I mentioned in another comment, the container has to have the filter on it as well, since it has a background-image property. Sorry for not making that clear in my original post!
    – SpaceMouse
    Commented Jul 5, 2015 at 17:52
  • Hm. Looks like it could be a solution, albeit a complicated one. Thanks!
    – SpaceMouse
    Commented Jul 6, 2015 at 3:20
3

use > to specify an image that is a child of .container the use not: to specify that you don't want the second image grey

.container > img:not(.two) {
     -webkit-filter: grayscale(100%);
    filter: grayscale(100%);
}
<div class="container">
 <img class="one" src="http://lorempixel.com/400/200" />
 <img class="two" src="http://lorempixel.com/400/200" />
 <img class="three" src="http://lorempixel.com/400/200" />
</div>

jsfiddle

.container > img:not(.two) {
 -webkit-filter: grayscale(100%);
filter: grayscale(100%);
 }
2

Use :not to exclude .two

The negation CSS pseudo-class, :not(X), is a functional notation taking a simple selector X as an argument. It matches an element that is not represented by the argument. X must not contain another negation selector.

.container img:not(.two) {
     -webkit-filter: grayscale(100%);
    filter: grayscale(100%);
}
1
  • This does not work. The container has to have the filter as well, so the filter applies to its background-image. There are also more containers here on higher levels that will also have the filter.
    – SpaceMouse
    Commented Jul 5, 2015 at 14:34
2

Seven years after this question was asked, I thought I'd come up with a brilliant CSS-native solution to this, using:

  • calc()
  • CSS Custom Properties

Hours of experimenting with CSS filter have satisfied me that the solution will never work.

Why not? Because functions like filter: hue-rotate() are both more complicated than you might expect and also, unhelpfully, unreliable.


My first ("clever") solution

(Calculate reverse transformations - cute, but doesn't work)

The starting point of my "clever" solution was:

It's well-established that once you apply filter to a parent element, that filter (much like opacity) continues to apply to all descendant elements and there is no way to mask a descendant element from that filter.

But filter simply describes transformations, right? And - surely - anything transformed can be un-transformed via a transformation which represents a mirror-image of the original?

Furthermore, if the original transformation is built in the right way from CSS Custom Properties, then it ought to be possible to build the mirror-image transformation using the same CSS Custom Properties and calc().

So I came up with something like this:

/*
OTHER CSS CUSTOM PROPERTIES (NOT NECESSARY FOR THIS EXAMPLE)

.square[data-theme="green"] {
  --saturation: 1;
  --contrast: 0.775;
  --brightness: 1.2;
}

.square[data-theme="blue"] {
  --saturation: 1;
  --contrast: 0.775;
  --brightness: 1.2;
}


.filter {
  --lightness: contrast(var(--contrast)) brightness(var(--brightness));
  --hsl-filter: hue-rotate(var(--hue)) saturate(var(--saturation)) var(--lightness);
}

.no-filter {
  --reverse-lightness: contrast(calc(1 / var(--contrast))) brightness(calc(1 / var(--brightness)));
  --reverse-hsl-filter: hue-rotate(calc(0deg - var(--hue))) saturate(calc(1 / var(--saturation))) var(--reverse-lightness);
}
*/
h2 {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 6;
  margin: 2px 0 0 2px;
  padding: 0;
  color: rgb(255, 255, 255);
  font-size: 12px;
  font-family: sans-serif;
  font-weight: 700;
}

.square {
  position: relative;
  float: left;
  display: inline-block;
  width: 92px;
  height: 92px;
  margin: 2px;
  padding: 6px;
  background-color: rgb(191, 0, 0);
  box-sizing: border-box;
}

.square:nth-of-type(4) {
  clear: left;
}

.circle {
  width: 80px;
  height: 80px;
  padding: 30px;
  background-color: rgb(255, 0, 0);
  border-radius: 50%;
  box-sizing: border-box;
}

.inner-square {
  width: 20px;
  height: 20px;
  background-color: rgb(255, 127, 0);
}

.square[data-theme="green"] {
  --hue: 112.5deg;
}

.square[data-theme="blue"] {
  --hue: 212.5deg;
}

.filter {
  --hsl-filter: hue-rotate(var(--hue));
  filter: var(--hsl-filter);
}

.no-filter {
  --reverse-hsl-filter: hue-rotate(calc(0deg - var(--hue)));
  filter: var(--reverse-hsl-filter);
}
<div class="square">
  <h2>Original</h2>
  <div class="circle">
    <div class="inner-square"></div>
  </div>
</div>

<div class="square filter" data-theme="green">
  <h2>Filtered</h2>
  <div class="circle">
    <div class="inner-square"></div>
  </div>
</div>

<div class="square filter" data-theme="green">
  <h2>No-Filter Test</h2>
  <div class="circle no-filter">
    <div class="inner-square"></div>
  </div>
</div>

<div class="square">
  <h2>Original</h2>
  <div class="circle">
    <div class="inner-square"></div>
  </div>
</div>

<div class="square filter" data-theme="blue">
  <h2>Filtered</h2>
  <div class="circle">
    <div class="inner-square"></div>
  </div>
</div>

<div class="square filter" data-theme="blue">
  <h2>No-Filter Test</h2>
  <div class="circle no-filter">
    <div class="inner-square"></div>
  </div>
</div>

It's less obvious in the top row (at first glance), but in the second row, the last square (ie. bottom right) clearly shows how this reverse-transformation approach is neither robust nor reliable:

  • The orange square in the bottom-right square isn't perfect, but it's close enough to the original
  • The orange square in the top-right square is less perfect, but it's still passable (just about)
  • The red circle in the top-right square isn't perfect, but it's close enough to the original
  • The red circle in the bottom-right square is no good at all

My second (less clever) solution

(Make the non-filtered element a sibling instead of a descendant element - less clever but it does work)

We may conclude from the above that the matrix transformation initiated by filter: hue-rotate() cannot be easily reversed - and that even if a computational way to reverse it consistently via JavaScript can be found - I'm currently doubtful over whether even that is possible - it's almost certainly not going to be possible via CSS calc().

Alternatively, we can turn the descendant elements we don't want to be affected by the filter into siblings of the element which has the CSS filter applied to it, instead:

h2 {
  position: absolute;
  top: 0;
  left: 0;
  z-index: 6;
  margin: 2px 0 0 2px;
  padding: 0;
  color: rgb(255, 255, 255);
  font-size: 12px;
  font-family: sans-serif;
  font-weight: 700;
}

.container {
  position: relative;
  float: left;
  display: inline-block;
  width: 92px;
  height: 92px;
  margin: 2px;
  background-color: rgb(0, 0, 0);
  box-sizing: border-box;
}

.container:nth-of-type(4) {
  clear: left;
}

.square {
  width: 92px;
  height: 92px;
  background-color: rgb(191, 0, 0);
}

.circle {
  position: absolute;
  top: 0;
  left: 0;
  width: 80px;
  height: 80px;
  margin: 6px;
  padding: 30px;
  background-color: rgb(255, 0, 0);
  border-radius: 50%;
  box-sizing: border-box;
}

.inner-square {
  width: 20px;
  height: 20px;
  background-color: rgb(255, 127, 0);
}

.container[data-theme="green"] {
  --hue: 112.5deg;
}

.container[data-theme="blue"] {
  --hue: 212.5deg;
}

.filter {
  --hsl-filter: hue-rotate(var(--hue));
  filter: var(--hsl-filter);
}
<div class="container">
  <h2>Original</h2>
  <div class="square"></div>
  <div class="circle">
    <div class="inner-square"></div>
  </div>
</div>

<div class="container" data-theme="green">
  <h2>Filtered</h2>
  <div class="square filter"></div>
  <div class="circle filter">
    <div class="inner-square"></div>
  </div>
</div>

<div class="container" data-theme="green">
  <h2>No-Filter Test</h2>
  <div class="square filter"></div>
  <div class="circle">
    <div class="inner-square"></div>
  </div>
</div>

<div class="container">
  <h2>Original</h2>
  <div class="square"></div>
  <div class="circle">
    <div class="inner-square"></div>
  </div>
</div>

<div class="container" data-theme="blue">
  <h2>Filtered</h2>
  <div class="square filter"></div>
  <div class="circle filter">
    <div class="inner-square"></div>
  </div>
</div>

<div class="container" data-theme="blue">
  <h2>No-Filter Test</h2>
  <div class="square filter"></div>
  <div class="circle">
    <div class="inner-square"></div>
  </div>
</div>

This second solution works perfectly, but it requires the HTML to be restructured and the CSS adjusted to compensate:

  • the filtered element from the original setup needs to be placed within a container element
  • the non-filtered descendant of the filtered element now needs to become a sibling of the filtered element, within the same container
  • finally, the non-filtered sibling needs to be re-positioned within the container so that it displays in the same place as before, back when it was a descendant

After taking some time to re-arrange markup and re-adjust styles, we can achieve the originally intended effect with some elements filtered and other elements non-filtered.

This second approach feels much less elegant than calculating mirror-image colour-transformations via CSS Custom Properties and calc() but until some kind of filter mask like:

  • filter-apply: all | none // or even (2 - n), (n + 3) etc.

is introduced into CSS...

... the only way for a child-element to be masked from a filter is to turn the child-element into a sibling-element.

2
  • 1
    This is the only solution that will work, particularly the second variant. Some filters can't be reversed, such as blur. My problem was worse, as I had an image as background of the parent div and I wanted to only blur that. Solution: turn the background into its own div (sibling to the children). Now I can blur that one and leave the others alone. I still can't understand answers like "incorporate the other answers" or, more generically, answers from people that did not even try before posting. Thank you @Rourin
    – Ricardo
    Commented Mar 31, 2023 at 13:24
  • I'm really happy this approach worked for you too, @Ricardo.
    – Rounin
    Commented Apr 3, 2023 at 12:23

Not the answer you're looking for? Browse other questions tagged or ask your own question.