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.
filter
, likeopacity
applies to the parent and *all *children and cannot be over-ridden by setting a competing style on a child element.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 thefilter
. It seems the only solution is to separate out the element(s) you don't want having afilter
out of the container element and managing positioning of the elements separate of the container. Or finding an alternative to usingfilter
itself