<header class="container">
<h1>Filter Utility Classes</h1>
<p class="lead">(Ab)using CSS variables to apply multiple filters with one utility</p>
</header>
<main class="container">
<p>The <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/filter"><kbd>filter</kbd> property</a> is a bit of an oddity when it comes to CSS.</p>
<p>Unlike most other properties, it can accept <strong>any number</strong> of named arguments, in <strong>any order</strong>, with each one being <strong>optional</strong>. This makes it extremely powerful and convenient, but a bit unorthodox to use, especially if you take a utility-first approach like <a href="https://tailwindcss.com/">TailwindCSS</a> for styling.</p>
<p>A typical utility class would employ a single property with a specific value. Here's an example of how that might look when defining blur and opacity classes:</p>
<pre><code>.filter-blur {
filter: blur(2px);
}
.filter-half-opacity {
filter: opacity(50%);
}</code></pre>
<p>However, both of these classes affect the same property! As <a href="https://www.smashingmagazine.com/2007/07/css-specificity-things-you-should-know/">CSS specificity</a> works, the latest defined property in this example (the <code>.filter-half-opacity</code> class) would take precedence, and the blurring would be ignored.</p>
<p>Luckily, we can write a new class that will use both of these at once!</p>
<pre><code>.filter-blur-and-half-opacity {
filter: blur(2px);
opacity(50%);
}</code></pre>
<p>While this works, it defeats the purpose of handy utility classes each providing just one function.</p>
<h2>In come CSS variables!</h2>
<p>With newly introduced CSS variables, we can work around this by defining a single class that <em>always has all filters applied</em>.</p>
<p>To do this, first we will have to also declare some defaults to fall back to, in case our element doesn't have all filters explicitly declared already. These defaults can simply be set to have no discerning visual effect on the element.</p>
<div class="row">
<div class="col">
<pre><code>:root {
--filter-blur: 0;
--filter-brightness: 100%;
--filter-contrast: 100%;
--filter-grayscale: 0%;
--filter-hue-rotate: 0deg;
--filter-invert: 0%;
--filter-opacity: 100%;
--filter-saturate: 100%;
--filter-sepia: 0%;
}</code></pre>
</div>
<div class="col">
<pre><code>.filter {
filter: blur(var(--filter-blur))
brightness(var(--filter-brightness))
contrast(var(--filter-contrast))
grayscale(var(--filter-grayscale))
hue-rotate(var(--filter-hue-rotate))
invert(var(--filter-invert))
opacity(var(--filter-opacity))
saturate(var(--filter-saturate))
sepia(var(--filter-sepia));
}</code></pre>
</div>
</div>
<p>From here on, each of our utility classes merely needs to override one of these variables in the element it's placed on. For example, rewriting the previous blur and opacity helpers:</p>
<pre><code>.filter-blur {
--filter-blur: blur(2px);
}
.filter-half-opacity {
--filter-opacity: opacity(50%);
}</code></pre>
<p>Now we can apply the <code>.filter</code> class to any element, then add any combination of our other filters as additional classes.</p>
<h2>Examples in use</h2>
<p>As an example of this in action, here is a photo (<a href="https://unsplash.com/photos/jORYUUvgfpA">taken by Cody Board</a>) with examples of different filters applied through utility classes.</p>
<div class="row">
<div class="col">
<figure class="figure">
<img src="https://images.unsplash.com/photo-1536685632249-7e210d6e381e?ixlib=rb-0.3.5&s=9de86971164db4588cd2b83de7e1f619&auto=format&fit=crop&w=800&q=80" class="figure-img img-fluid rounded filter filter-blur-5" alt="Outstreched hand over a landscape">
<figcaption class="figure-caption"><code>.filter .filter-blur-5</code></figcaption>
</figure>
</div>
<div class="col">
<figure class="figure">
<img src="https://images.unsplash.com/photo-1536685632249-7e210d6e381e?ixlib=rb-0.3.5&s=9de86971164db4588cd2b83de7e1f619&auto=format&fit=crop&w=800&q=80" class="figure-img img-fluid rounded filter filter-saturate-50 filter-sepia-90" alt="Outstreched hand over a landscape">
<figcaption class="figure-caption"><code>.filter .filter-saturate-50 .filter-sepia-90</code></figcaption>
</figure>
</div>
</div>
<div class="row">
<div class="col">
<figure class="figure">
<img src="https://images.unsplash.com/photo-1536685632249-7e210d6e381e?ixlib=rb-0.3.5&s=9de86971164db4588cd2b83de7e1f619&auto=format&fit=crop&w=800&q=80" class="figure-img img-fluid rounded filter filter-blur-5 filter-opacity-50" alt="Outstreched hand over a landscape">
<figcaption class="figure-caption"><code>.filter .filter-blur-5 .filter-opacity-50</code></figcaption>
</figure>
</div>
<div class="col">
<figure class="figure">
<img src="https://images.unsplash.com/photo-1536685632249-7e210d6e381e?ixlib=rb-0.3.5&s=9de86971164db4588cd2b83de7e1f619&auto=format&fit=crop&w=800&q=80" class="figure-img img-fluid rounded filter filter-hue-rotate-45 filter-contrast-200" alt="Outstreched hand over a landscape">
<figcaption class="figure-caption"><code>.filter .filter-hue-rotate-45 .filter-contrast-200</code></figcaption>
</figure>
</div>
</div>
<h2>Sass example</h2>
<p>In the CSS tab of this Pen, you can find an example Sass script to generate a handful of these classes for you that were used in the examples above.</p>
<p>Take a look at the compiled CSS output to see what classes get generated.</p>
<h2>Wrapping up</h2>
<p>While this solution works pretty well, there are a couple of points to note about the approach…</p>
<ul>
<li>This is merely a proof-of-concept solution for a problem with CSS filters</li>
<li><a href="https://caniuse.com/css-variables">Browser support</a> for CSS variables still isn't <em>quite</em> there to rely on</li>
<li>This probably isn't the most performant solution, since all filters are always applied</li>
<li>If you generate a ton of classes like this, you may wish to use <a href="https://github.com/FullHuman/purgecss">PurgeCSS</a> to still get a relatively small CSS output</li>
<li>You could remove the <code>.filter</code> class itself by having the filter property declared inside each individual utility class itself. This may make it a lot more easier to use as a developer, but would result in <em>huge</em> generated CSS.</li>
</ul>
</main>
/*
|--------------------------------------------------------------------------
| Filters
|--------------------------------------------------------------------------
|
| Utility classes for various CSS filters that can be combined together
| using CSS variables. When using, the `.filter` class must be used,
| on an element along with another such as `.filter-blur-1` to set
| the filter.
|
*/
$filters: (
"blur": "px",
"brightness": "%",
"contrast": "%",
"grayscale": "%",
"hue-rotate": "deg",
"invert": "%",
"opacity": "%",
"saturate": "%",
"sepia": "%",
);
$filter-levels: (
"%": (0 10 20 30 40 50 60 70 80 90 100 200),
"deg": (0 45 90 135 180 225 270 315 360),
"px": (0, 1, 2, 3, 4, 5, 10),
);
// Note: CSS variables must have a value passed to them, so a
// default for each possible filter function is provided as
// a fallback if one is an element does have one declared.
:root {
--filter-blur: 0;
--filter-brightness: 100%;
--filter-contrast: 100%;
--filter-grayscale: 0%;
--filter-hue-rotate: 0deg;
--filter-invert: 0%;
--filter-opacity: 100%;
--filter-saturate: 100%;
--filter-sepia: 0%;
}
// Note: These filter definitions are escaped as strings. This is so as to not
// cause conflicts between CSS filter names that share the same names as
// native Sass functions, or cause any compiling concatenation issues.
.filter {
filter: #{"blur(var(--filter-blur))"}
#{"brightness(var(--filter-brightness))"}
#{"contrast(var(--filter-contrast))"}
#{"grayscale(var(--filter-grayscale))"}
#{"hue-rotate(var(--filter-hue-rotate))"}
#{"invert(var(--filter-invert))"}
#{"opacity(var(--filter-opacity))"}
#{"saturate(var(--filter-saturate))"}
#{"sepia(var(--filter-sepia))"};
}
@each $filter, $unit in $filters {
@each $filter-level in map-get($filter-levels, $unit) {
.filter-#{$filter}-#{$filter-level} {
--filter-#{$filter}: #{$filter-level}#{$unit};
}
}
}
.no-filter {
filter: none;
}
View Compiled
/*
|--------------------------------------------------------------------------
| No JavaScript Required!
|--------------------------------------------------------------------------
|
| This is all native CSS baby!
|
| If you want to discuss this idea or follow
| me, you can find me on Twitter at:
| https://twitter.com/liamhammett
|
*/
This Pen doesn't use any external JavaScript resources.