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.