filter – CSS-Tricks https://css-tricks.com Tips, Tricks, and Techniques on using Cascading Style Sheets. Wed, 26 Oct 2022 17:05:29 +0000 en-US hourly 1 https://wordpress.org/?v=6.1.1 https://i0.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&ssl=1 filter – CSS-Tricks https://css-tricks.com 32 32 45537868 Holographic Trading Card Effect https://css-tricks.com/holographic-trading-card-effect/ https://css-tricks.com/holographic-trading-card-effect/#comments Wed, 26 Oct 2022 17:05:28 +0000 https://css-tricks.com/?p=374609 Simon Goellner (@simeydotme)’s collection of Holographic Trading Cards have captured our attention.

Under the hood there is a suite of filter(), background-blend-mode(), mix-blend-mode(), and clip-path() combinations that have been painstakingly tweaked to reach the desired effect. I …


Holographic Trading Card Effect originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Simon Goellner (@simeydotme)’s collection of Holographic Trading Cards have captured our attention.

Under the hood there is a suite of filter(), background-blend-mode(), mix-blend-mode(), and clip-path() combinations that have been painstakingly tweaked to reach the desired effect. I ended up using a little img { visibility: hidden; } in DevTools to get a better sense of each type of holographic effect.

Josh Dance (@JoshDance) replied with a breakdown of the effects that lets you manually control the inputs.

To Shared LinkPermalink on CSS-Tricks


Holographic Trading Card Effect originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/holographic-trading-card-effect/feed/ 4 374609
Creating Realistic Reflections With CSS https://css-tricks.com/creating-realistic-reflections-with-css/ https://css-tricks.com/creating-realistic-reflections-with-css/#comments Mon, 02 May 2022 14:14:07 +0000 https://css-tricks.com/?p=365491 In design, reflections are stylized mirror images of objects. Even though they are not as popular as shadows, they have their moments — just think about the first time you explored the different font formats in MS Word or PowerPoint: …


Creating Realistic Reflections With CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
In design, reflections are stylized mirror images of objects. Even though they are not as popular as shadows, they have their moments — just think about the first time you explored the different font formats in MS Word or PowerPoint: I bet reflection was your second-most-used style, next to shadow, foregoing others like outline and glow. Or perhaps you remember when reflections were all the rage back when Apple used them on just about everything.

Old iPod Touch product image showing the front, back, and profile of the device with subtle reflections beneath each.

Reflections are still cool! And unlike years past, we can actually make reflections with CSS! Here’s what we’ll be making in this article:

There are two steps to a reflection design:

  1. Create a copy of the original design.
  2. Style that copy.

The most authentic and standardized way to get a mirror image in CSS now would be to use the element() property. But it’s still in its experimental phase and is only supported in Firefox, at the time of writing this article. If you’re curious, you can check out this article I wrote that experiments with it.

So, rather than element(), I’m going to add two of the same designs and use one as the reflection in my examples. You can code this part to be dynamic using JavaScript, or use pseudo-elements, but in my demos, I’m using a pair of identical elements per design.

<div class="units">
  <div>trinket</div>
  <div>trinket</div>
</div>
.units > * {
  background-image: url('image.jpeg');
  background-clip: text;
  color: transparent;
  /* etc. */
}

The original design is a knock-out text graphic created from the combination of a background image, transparent text color, and the background-clip property with its text value.

The bottom element of the pair is then turned upside-down and moved closer to the original design using transform. This is the reflection:

.units > :last-child {
  transform: rotatex(180deg) translatey(15px); 
}

The now upturned bottom element will take on some styles to create fades and other graphic effects on the reflection. A gradual fading of reflection can be achieved with a linear gradient image used as a mask layer on the upturned element.

.units > :last-child {
  transform: rotatex(180deg) translatey(15px); 
  mask-image: linear-gradient(transparent 50%, white 90%);
}

By default, the mask-mode of the mask-image property is alpha. That means the transparent parts of an image, when the image is used as a mask layer for an element, turn their corresponding areas of the element transparent as well. That’s why a linear-gradient with transparent gradation at the top fades out the upside-down reflection at the end.

We can also try other gradient styles, with or without combining them. Take this one with stripes, for example. I added the pattern along with the fade effect from before.

.units > :last-child {
  /* ... */
  mask-image: 
    repeating-linear-gradient(transparent, transparent 3px, white 3px, white 4px),
    linear-gradient( transparent 50%, white 90%);
}

Or this one with radial-gradient:

.units > :last-child {
  /* ... */
  mask-image: radial-gradient(circle at center, white, transparent 50%);
}

Another idea is to morph the mirror image by adding skew() to the transform property. This gives some movement to the reflection.

.units > :last-child {
  /* ... */
  transform: rotatex(180deg) translatey(15px) skew(135deg) translatex(30px);
}

When you need the reflection to be subtle and more like a shadow, then blurring it out, brightening it, or reducing its opacity, can do the trick.

.units > :last-child {
  /* ... */
  filter: blur(4px) brightness(1.5);
}

Sometimes a reflection can also be shadowy itself, so, instead of using the background image (from the original design) or a block color for the text, I tried giving the reflection a series of translucent shadows of red, blue and green colors that go well with the original design.

.units > :last-child {
  /* ... */
  text-shadow: 
    0 0 8px rgb(255 0 0 / .4),
    -2px -2px 6px rgb(0 255 0 / .4),
    2px 2px 4px rgb(0 255 255 / .4);
}

Do those rgb()values look weird? That’s a new syntax that’s part of some exciting new CSS color features.

Let’s bring all of these approaches together in one big demo:

Wrapping up

The key to a good reflection is to go with effects that are subtler than the main object, but not so subtle that it’s difficult to notice. Then there are other considerations, including the reflection’s color, direction, and shape.

I hope you got some inspirations from this! Sure, all we looked at here was text, but reflections can work well for any striking element in a design that has a sensible enough space around it and can benefit from a reflection to elevate itself on the page.


Creating Realistic Reflections With CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/creating-realistic-reflections-with-css/feed/ 6 365491
Icon Glassmorphism Effect in CSS https://css-tricks.com/icon-glassmorphism-effect-in-css/ https://css-tricks.com/icon-glassmorphism-effect-in-css/#comments Mon, 08 Nov 2021 14:57:42 +0000 https://css-tricks.com/?p=322098 I recently came across a cool effect known as glassmorphism in a Dribble shot. My first thought was I could quickly recreate it in a few minutes if I just use some emojis for the icons without wasting time …


Icon Glassmorphism Effect in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I recently came across a cool effect known as glassmorphism in a Dribble shot. My first thought was I could quickly recreate it in a few minutes if I just use some emojis for the icons without wasting time on SVG-ing them.

Animated gif. Shows a nav bar with four grey icons. On :hover/ :focus, a tinted icon slides and rotates, partly coming out from behind the grey one. In the area where they overlap, we have a glassmorphism effect, with the icon in the back seen as blurred through the semitransparent grey one in front.
The effect we’re after.

I couldn’t have been more wrong about those “few minutes” — they ended up being days of furiously and frustratingly scratching this itch!

It turns out that, while there are resources on how to CSS such an effect, they all assume the very simple case where the overlay is rectangular or at most a rectangle with border-radius. However, getting a glassmorphism effect for irregular shapes like icons, whether these icons are emojis or proper SVGs, is a lot more complicated than I expected, so I thought it would be worth sharing the process, the traps I fell into and the things I learned along the way. And also the things I still don’t understand.

Why emojis?

Short answer: because SVG takes too much time. Long answer: because I lack the artistic sense of just drawing them in an image editor, but I’m familiar with the syntax enough such that I can often compact ready-made SVGs I find online to less than 10% of their original size. So, I cannot just use them as I find them on the internet — I have to redo the code to make it super clean and compact. And this takes time. A lot of time because it’s detail work.

And if all I want is to quickly code a menu concept with icons, I resort to using emojis, applying a filter on them in order to make them match the theme and that’s it! It’s what I did for this liquid tab bar interaction demo — those icons are all emojis! The smooth valley effect makes use of the mask compositing technique.

Animated gif. Shows a white liquid navigation bar with five items, one of which is selected. The selected one has a smooth valley at the top, with a dot levitating above it. It's also black, while the non-selected ones are grey in the normal state and beige in the :hover/ :focus state. On clicking another icon, the selection smoothly changes as the valley an the levitating dot slide to always be above the currently selected item.
Liquid navigation.

Alright, so this is going to be our starting point: using emojis for the icons.

The initial idea

My first thought was to stack the two pseudos (with emoji content) of the navigation links, slightly offset and rotate the bottom one with a transform so that they only partly overlap. Then, I’d make the top one semitransparent with an opacity value smaller than 1, set backdrop-filter: blur() on it, and that should be just about enough.

Now, having read the intro, you’ve probably figured out that didn’t go as planned, but let’s see what it looks like in code and what issues there are with it.

We generate the nav bar with the following Pug:

- let data = {
-   home: { ico: '🏠', hue: 200 }, 
-   notes: { ico: '🗒️', hue: 260 }, 
-   activity: { ico: '🔔', hue: 320 }, 
-   discovery: { ico: '🧭', hue: 30 }
- };
- let e = Object.entries(data);
- let n = e.length;

nav
  - for(let i = 0; i > n; i++)
    a(href='#' data-ico=e[i][1].ico style=`--hue: ${e[i][1].hue}deg`) #{e[i][0]}

Which compiles to the HTML below:

<nav>
  <a href='#' data-ico='🏠' style='--hue: 200deg'>home</a>
  <a href='#' data-ico='🗒️' style='--hue: 260deg'>notes</a>
  <a href='#' data-ico='🔔' style='--hue: 320deg'>activity</a>
  <a href='#' data-ico='🧭' style='--hue: 30deg'>iscovery</a>
</nav>

We start with layout, making our elements grid items. We place the nav in the middle, give links explicit widths, put both pseudos for each link in the top cell (which pushes the link text content to the bottom cell) and middle-align the link text and pseudos.

body, nav, a { display: grid; }

body {
  margin: 0;
  height: 100vh;
}

nav {
  grid-auto-flow: column;
  place-self: center;
  padding: .75em 0 .375em;
}

a {
  width: 5em;
  text-align: center;
  
  &::before, &::after {
    grid-area: 1/ 1;
    content: attr(data-ico);
  }
}
Screenshot. Shows the four menu items lined up in a row in the middle of the page, each item occupying a column, all columns having the same width; with emojis above the link text, both middle-aligned horizontally.
Firefox screenshot of the result after we got layout basics sorted.

Note that the look of the emojis is going to be different depending on the browser you’re using view the demos.

We pick a legible font, bump up its size, make the icons even bigger, set backgrounds, and a nicer color for each of the links (based on the --hue custom property in the style attribute of each):

body {
  /* same as before */
  background: #333;
}

nav {
  /* same as before */
  background: #fff;
  font: clamp(.625em, 5vw, 1.25em)/ 1.25 ubuntu, sans-serif;
}

a {
  /* same as before */
  color: hsl(var(--hue), 100%, 50%);
  text-decoration: none;
  
  &::before, &::after {
    /* same as before */
    font-size: 2.5em;
  }
}
Screenshot. Shows the same layout as before, only with a prettier and bigger font and even bigger icons, backgrounds and each menu item having a different color value based on its --hue.
Chrome screenshot of the result (live demo) after prettifying things a bit.

Here’s where things start to get interesting because we start differentiating between the two emoji layers created with the link pseudos. We slightly move and rotate the ::before pseudo, make it monochrome with a sepia(1) filter, get it to the right hue, and bump up its contrast() — an oldie but goldie technique from Lea Verou. We also apply a filter: grayscale(1) on the ::after pseudo and make it semitransparent because, otherwise, we wouldn’t be able to see the other pseudo through it.

a {
  /* same as before */
  
  &::before {
    transform: 
      translate(.375em, -.25em) 
      rotate(22.5deg);
    filter: 
      sepia(1) 
      hue-rotate(calc(var(--hue) - 50deg)) 
      saturate(3);
  }
	
  &::after {
    opacity: .5;
    filter: grayscale(1);
  }
}
Screenshot. Same nav bar as before, only now the top icon layer is grey and semitransparent, while the bottom one is slightly offset and rotated, mono in the specified --hue.
Chrome screenshot of the result (live demo) after differentiating between the two icon layers.

Hitting a wall

So far, so good… so what? The next step, which I foolishly thought would be the last when I got the idea to code this, involves setting a backdrop-filter: blur(5px) on the top (::after) layer.

Note that Firefox still needs the gfx.webrender.all and layout.css.backdrop-filter.enabled flags set to true in about:config in order for the backdrop-filter property to work.

Animated gif. Shows how to find the flags mentioned above (gfx.webrender.all and layout.css.backdrop-filter.enabled) in order to ensure they are set to true. Go to about:config, start typing their name in the search box and double click their value to change it if it's not set to true already.
The flags that are still required in Firefox for backdrop-filter to work.

Sadly, the result looks nothing like what I expected. We get a sort of overlay the size of the entire top icon bounding box, but the bottom icon isn’t really blurred.

Screenshot collage. Shows the not really blurred, but awkward result with an overlay the size of the top emoji box after applying the backdrop-filter property. This happens both in Chrome (top) and in Firefox (bottom).
Chrome (top) and Firefox (bottom) screenshots of the result (live demo) after applying backdrop-filter.

However, I’m pretty sure I’ve played with backdrop-filter: blur() before and it worked, so what the hairy heck is going on here?

Screenshot. Shows a working glassmorphism effect, created via a control panel where we draw some sliders to get the value for each filter function.
Working glassmorphism effect (live demo) in an older demo I coded.

Getting to the root of the problem

Well, when you have no idea whatsoever why something doesn’t work, all you can do is take another working example, start adapting it to try to get the result you want… and see where it breaks!

So let’s see a simplified version of my older working demo. The HTML is just an article in a section. In the CSS, we first set some dimensions, then we set an image background on the section, and a semitransparent one on the article. Finally, we set the backdrop-filter property on the article.

section { background: url(cake.jpg) 50%/ cover; }

article {
  margin: 25vmin;
  height: 40vh;
  background: hsla(0, 0%, 97%, .25);
  backdrop-filter: blur(5px);
}
Screenshot. Shows a working glassmorphism effect, where we have a semitransparent box on top of its parent one, having an image background.
Working glassmorphism effect (live demo) in a simplified test.

This works, but we don’t want our two layers nested in one another; we want them to be siblings. So, let’s make both layers article siblings, make them partly overlap and see if our glassmorphism effect still works.

<article class='base'></article>
<article class='grey'></article>
article { width: 66%; height: 40vh; }

.base { background: url(cake.jpg) 50%/ cover; }

.grey {
  margin: -50% 0 0 33%;
  background: hsla(0, 0%, 97%, .25);
  backdrop-filter: blur(5px);
}
Screenshot collage. Shows the case where we have a semitransparent box on top of its sibling having an image background. The top panel screenshot was taken in Chrome, where the glassmorphism effect works as expected. The bottom panel screenshot was taken in Firefox, where things are mostly fine, but the blur handling around the edges is really weird.
Chrome (top) and Firefox (bottom) screenshots of the result (live demo) when the two layers are siblings.

Everything still seems fine in Chrome and, for the most part, Firefox too. It’s just that the way blur() is handled around the edges in Firefox looks awkward and not what we want. And, based on the few images in the spec, I believe the Firefox result is also incorrect?

I suppose one fix for the Firefox problem in the case where our two layers sit on a solid background (white in this particular case) is to give the bottom layer (.base) a box-shadow with no offsets, no blur, and a spread radius that’s twice the blur radius we use for the backdrop-filter applied on the top layer (.grey). Sure enough, this fix seems to work in our particular case.

Things get a lot hairier if our two layers sit on an element with an image background that’s not fixed (in which case, we could use a layered backgrounds approach to solve the Firefox issue), but that’s not the case here, so we won’t get into it.

Still, let’s move on to the next step. We don’t want our two layers to be two square boxes, we want then to be emojis, which means we cannot ensure semitransparency for the top one using a hsla() background — we need to use opacity.

.grey {
  /* same as before */
  opacity: .25;
  background: hsl(0, 0%, 97%);
}
Screenshot. Shows the case where we have a subunitary opacity on the top layer in order to make it semitransparent, instead of a subunitary alpha value for the semitransparent background.
The result (live demo) when the top layer is made semitransparent using opacity instead of a hsla() background.

It looks like we found the problem! For some reason, making the top layer semitransparent using opacity breaks the backdrop-filter effect in both Chrome and Firefox. Is that a bug? Is that what’s supposed to happen?

Bug or not?

MDN says the following in the very first paragraph on the backdrop-filter page:

Because it applies to everything behind the element, to see the effect you must make the element or its background at least partially transparent.

Unless I don’t understand the above sentence, this appears to suggest that opacity shouldn’t break the effect, even though it does in both Chrome and Firefox.

What about the spec? Well, the spec is a huge wall of text without many illustrations or interactive demos, written in a language that makes reading it about as appealing as sniffing a skunk’s scent glands. It contains this part, which I have a feeling might be relevant, but I’m unsure that I understand what it’s trying to say — that the opacity set on the top element that we also have the backdrop-filter on also gets applied on the sibling underneath it? If that’s the intended result, it surely isn’t happening in practice.

The effect of the backdrop-filter will not be visible unless some portion of element B is semi-transparent. Also note that any opacity applied to element B will be applied to the filtered backdrop image as well.

Trying random things

Whatever the spec may be saying, the fact remains: making the top layer semitransparent with the opacity property breaks the glassmorphism effect in both Chrome and Firefox. Is there any other way to make an emoji semitransparent? Well, we could try filter: opacity()!

At this point, I should probably be reporting whether this alternative works or not, but the reality is… I have no idea! I spent a couple of days around this step and got to check the test countless times in the meanwhile — sometimes it works, sometimes it doesn’t in the exact same browsers, wit different results depending on the time of day. I also asked on Twitter and got mixed answers. Just one of those moments when you can’t help but wonder whether some Halloween ghost isn’t haunting, scaring and scarring your code. For eternity!

It looks like all hope is gone, but let’s try just one more thing: replacing the rectangles with text, the top one being semitransparent with color: hsla(). We may be unable to get the cool emoji glassmorphism effect we were after, but maybe we can get such a result for plain text.

So we add text content to our article elements, drop their explicit sizing, bump up their font-size, adjust the margin that gives us partial overlap and, most importantly, replace the background declarations in the last working version with color ones. For accessibility reasons, we also set aria-hidden='true' on the bottom one.

<article class='base' aria-hidden='true'>Lion 🧡</article>
<article class='grey'>Lion 🖤</article>
article { font: 900 21vw/ 1 cursive; }

.base { color: #ff7a18; }

.grey {
  margin: -.75em 0 0 .5em;
  color: hsla(0, 0%, 50%, .25);
  backdrop-filter: blur(5px);
}
Screenshot collage. Shows the case where we have a semitransparent text layer on top of its identical solid orange text sibling. The top panel screenshot was taken in Chrome, where we get proper blurring, but it's underneath the entire bounding box of the semitransparent top text, not limited to just the actual text. The bottom panel screenshot was taken in Firefox, where things are even worse, with the blur handling around the edges being really weird.
Chrome (top) and Firefox (bottom) screenshots of the result (live demo) when we have two text layers.

There are couple of things to note here.

First, setting the color property to a value with a subunitary alpha also makes emojis semitransparent, not just plain text, both in Chrome and in Firefox! This is something I never knew before and I find absolutely mindblowing, given the other channels don’t influence emojis in any way.

Second, both Chrome and Firefox are blurring the entire area of the orange text and emoji that’s found underneath the bounding box of the top semitransparent grey layer, instead of just blurring what’s underneath the actual text. In Firefox, things look even worse due to that awkward sharp edge effect.

Even though the box blur is not what we want, I can’t help but think it does make sense since the spec does say the following:

[…] to create a “transparent” element that allows the full filtered backdrop image to be seen, you can use “background-color: transparent;”.

So let’s make a test to check what happens when the top layer is another non-rectangular shape that’s not text, but instead obtained with a background gradient, a clip-path or a mask!

Screenshot collage. Shows the case where we have semitransparent non-rectangular shaped layers (obtained with three various methods: gradient background, clip-path and mask) on top of a rectangular siblings. The top panel screenshot was taken in Chrome, where things seem to work fine in the clip-path and mask case, but not in the gradient background case. In this case, everything that's underneath the bounding box of the top element gets blurred, not just what's underneath the visible part. The bottom panel screenshot was taken in Firefox, where, regardless of the way we got the shape, everything underneath its bounding box gets blurred, not just what's underneath the actual shape. Furthermore, in all three cases we have the old awkward sharp edge issue we've had in Firefox before
Chrome (top) and Firefox (bottom) screenshots of the result (live demo) when the top layer is a non-rectangular shape.

In both Chrome and Firefox, the area underneath the entire box of the top layer gets blurred when the shape is obtained with background: gradient() which, as mentioned in the text case before, makes sense per the spec. However, Chrome respects the clip-path and mask shapes, while Firefox doesn’t. And, in this case, I really don’t know which is correct, though the Chrome result does make more sense to me.

Moving towards a Chrome solution

This result and a Twitter suggestion I got when I asked how to make the blur respect the text edges and not those of its bounding box led me to the next step for Chrome: applying a mask clipped to the text on the top layer (.grey). This solution doesn’t work in Firefox for two reasons: one, text is sadly a non-standard mask-clip value that only works in WebKit browsers and, two, as shown by the test above, masking doesn’t restrict the blur area to the shape created by the mask in Firefox anyway.

/* same as before */

.grey {
  /* same as before */
  -webkit-mask: linear-gradient(red, red) text; /* only works in WebKit browsers */
}
Chrome screenshot. Shows two text and emoji layers partly overlapping. The top one is semitransparent, so through it, we can see the layer underneath blurred (by applying a backdrop-filter on the top one).
Chrome screenshot of the result (live demo) when the top layer has a mask restricted to the text area.

Alright, this actually looks like what we want, so we can say we’re heading in the right direction! However, here we’ve used an orange heart emoji for the bottom layer and a black heart emoji for the top semitransparent layer. Other generic emojis don’t have black and white versions, so my next idea was to initially make the two layers identical, then make the top one semitransparent and use filter: grayscale(1) on it.

article { 
  color: hsla(25, 100%, 55%, var(--a, 1));
  font: 900 21vw/ 1.25 cursive;
}

.grey {
  --a: .25;
  margin: -1em 0 0 .5em;
  filter: grayscale(1);
  backdrop-filter: blur(5px);
  -webkit-mask: linear-gradient(red, red) text;
}
Chrome screenshot. Shows two text and emoji layers partly overlapping. The top one is semitransparent, so through it, we can see the layer underneath blurred (by applying a backdrop-filter on the top one). The problem is that applying the grayscale filter on the top semitransparent layer not only affects this layer, but also the blurred area of the layer underneath.
Chrome screenshot of the result (live demo) when the top layer gets a grayscale(1) filter.

Well, that certainly had the effect we wanted on the top layer. Unfortunately, for some weird reason, it seems to have also affected the blurred area of the layer underneath. This moment is where to briefly consider throwing the laptop out the window… before getting the idea of adding yet another layer.

It would go like this: we have the base layer, just like we have so far, slightly offset from the other two above it. The middle layer is a “ghost” (transparent) one that has the backdrop-filter applied. And finally, the top one is semitransparent and gets the grayscale(1) filter.

body { display: grid; }

article {
  grid-area: 1/ 1;
  place-self: center;
  padding: .25em;
  color: hsla(25, 100%, 55%, var(--a, 1));
  font: 900 21vw/ 1.25 pacifico, z003, segoe script, comic sans ms, cursive;
}

.base { margin: -.5em 0 0 -.5em; }

.midl {
  --a: 0;
  backdrop-filter: blur(5px);
  -webkit-mask: linear-gradient(red, red) text;
}

.grey { filter: grayscale(1) opacity(.25) }
Chrome screenshot. Shows two text and emoji layers partly overlapping. The top one is semitransparent grey, so through it, we can see the layer underneath blurred (by applying a backdrop-filter on a middle, completely transparent one).
Chrome screenshot of the result (live demo) with three layers.

Now we’re getting somewhere! There’s just one more thing left to do: make the base layer monochrome!

/* same as before */

.base {
  margin: -.5em 0 0 -.5em;
  filter: sepia(1) hue-rotate(165deg) contrast(1.5);
}
Chrome screenshot. Shows two text and emoji layers partly overlapping. The bottom one is mono (bluish in this case) and blurred at the intersection with the semitransparent grey one on top.
Chrome screenshot of the result (live demo) we were after.

Alright, this is the effect we want!

Getting to a Firefox solution

While coding the Chrome solution, I couldn’t help but think we may be able to pull off the same result in Firefox since Firefox is the only browser that supports the element() function. This function allows us to take an element and use it as a background for another element.

The idea is that the .base and .grey layers will have the same styles as in the Chrome version, while the middle layer will have a background that’s (via the element() function) a blurred version of our layers.

To make things easier, we start with just this blurred version and the middle layer.

<article id='blur' aria-hidden='true'>Lion 🦁</article>
<article class='midl'>Lion 🦁</article>

We absolutely position the blurred version (still keeping it in sight for now), make it monochrome and blur it and then use it as a background for .midl.

#blur {
  position: absolute;
  top: 2em; right: 0;
  margin: -.5em 0 0 -.5em;
  filter: sepia(1) hue-rotate(165deg) contrast(1.5) blur(5px);
}

.midl {
  --a: .5;
  background: -moz-element(#blur);
}

We’ve also made the text on the .midl element semitransparent so we can see the background through it. We’ll make it fully transparent eventually, but for now, we still want to see its position relative to the background.

Firefox screenshot. Shows a blurred mono (bluish in this case) text and emoji element below everything else. 'Everything else' in this case is another text and emoji element that uses a semitransparent color so we can partly see through to the background which is set to the blurred element via the element() function.
Firefox screenshot of the result (live demo) when using the blurred element #blur as a background.

We can notice a one issue right away: while margin works to offset the actual #blur element, it does nothing for shifting its position as a background. In order to get such an effect, we need to use the transform property. This can also help us if we want a rotation or any other transform — as it can be seen below where we’ve replaced the margin with transform: rotate(-9deg).

Firefox screenshot. Shows a slightly rotated blurred mono (bluish in this case) text and emoji element below everything else. 'Everything else' in this case is another text and emoji element that uses a semitransparent color so we can partly see through to the background which is set to the slightly rotated blurred element via the element() function.
Firefox screenshot of the result (live demo) when using transform: rotate() instead of margin on the #blur element.

Alright, but we’re still sticking to just a translation for now:

#blur {
  /* same as before */
  transform: translate(-.25em, -.25em); /* replaced margin */
}
Firefox screenshot. Shows a slightly offset blurred mono (bluish in this case) text and emoji element below everything else. 'Everything else' in this case is another text and emoji element that uses a semitransparent color so we can partly see through to the background which is set to the slightly offset blurred element via the element() function. This slight offset means the actual text doesn't perfectly overlap with the background one anymore.
Firefox screenshot of the result (live demo) when using transform: translate() instead of margin on the #blur element.

One thing to note here is that a bit of the blurred background gets cut off as it goes outside the limits of the middle layer’s padding-box. That doesn’t matter at this step anyway since our next move is to clip the background to the text area, but it’s good to just have that space since the .base layer is going to get translated just as far.

Firefox screenshot. Shows a slightly offset blurred mono (bluish in this case) text and emoji element below everything else. 'Everything else' in this case is another text and emoji element that uses a semitransparent color so we can partly see through to the background which is set to the slightly offset blurred element via the element() function. This slight offset means the actual text doesn't perfectly overlap with the background one anymore. It also means that the translated background text may not fully be within the limits of the padding-box anymore, as highlighted in this screenshot, which also shows the element boxes overlays.
Firefox screenshot highlighting how the translated #blur background exceeds the limits of the padding-box on the .midl element.

So, we’re going to bump up the padding by a little bit, even if, at this point, it makes absolutely no difference visually as we’re also setting background-clip: text on our .midl element.

article {
  /* same as before */
  padding: .5em;
}

#blur {
  position: absolute;
  bottom: 100vh;
  transform: translate(-.25em, -.25em);
  filter: sepia(1) hue-rotate(165deg) contrast(1.5) blur(5px);
}

.midl {
  --a: .1;
  background: -moz-element(#blur);
  background-clip: text;
}

We’ve also moved the #blur element out of sight and further reduced the alpha of the .midl element’s color, as we want a better view at the background through the text. We’re not making it fully transparent, but still keeping it visible for now just so we know what area it covers.

Firefox screenshot. Shows a text and emoji element that uses a semitransparent color so we can partly see through to the background which is set to a blurred element (now positioned out of sight) via the element() function. This slight offset means the actual text doesn't perfectly overlap with the background one anymore. We have also clipped the background of this element to the text, so that none of the background outside it is visible. Even so, there's enough padding room so that the blurred background is contained within the padding-box.
Firefox screenshot of the result (live demo) after clipping the .midl element’s background to text.

The next step is to add the .base element with pretty much the same styles as it had in the Chrome case, only replacing the margin with a transform.

<article id='blur' aria-hidden='true'>Lion 🦁</article>
<article class='base' aria-hidden='true'>Lion 🦁</article>
<article class='midl'>Lion 🦁</article>
#blur {
  position: absolute;
  bottom: 100vh;
  transform: translate(-.25em, -.25em);
  filter: sepia(1) hue-rotate(165deg) contrast(1.5) blur(5px);
}

.base {
  transform: translate(-.25em, -.25em);
  filter: sepia(1) hue-rotate(165deg) contrast(1.5);
}

Since a part of these styles are common, we can also add the .base class on our blurred element #blur in order to avoid duplication and reduce the amount of code we write.

<article id='blur' class='base' aria-hidden='true'>Lion 🦁</article>
<article class='base' aria-hidden='true'>Lion 🦁</article>
<article class='midl'>Lion 🦁</article>
#blur {
  --r: 5px;
  position: absolute;
  bottom: 100vh;
}

.base {
  transform: translate(-.25em, -.25em);
  filter: sepia(1) hue-rotate(165deg) contrast(1.5) blur(var(--r, 0));
}
Firefox screenshot. Shows two text and emoji layers slightly offset from one another. The .base one, first in the DOM order, is made mono with a filter and slightly offset to the top left with a transform. The .midl one, following it in DOM order, has semitransparent text so that we can see through to the text clipped background, which uses as a background image the blurred version of the mono, slightly offset .base layer. In spite of DOM order, the .base layer still shows up on top.
Firefox screenshot of the result (live demo) after adding the .base layer.

We have a different problem here. Since the .base layer has a transform, it’s now on top of the .midl layer in spite of DOM order. The simplest fix? Add z-index: 2 on the .midl element!

Firefox screenshot. Shows two text and emoji layers slightly offset from one another. The .base one, first in the DOM order, is made mono with a filter and slightly offset to the top left with a transform. The .midl one, following it in DOM order, has semitransparent text so that we can see through to the text clipped background, which uses as a background image the blurred version of the mono, slightly offset .base layer. Having explicitly set a z-index on the .midl layer, it now shows up on top of the .base one.
Firefox screenshot of the result (live demo) after fixing the layer order such that .base is underneath .midl.

We still have another, slightly more subtle problem: the .base element is still visible underneath the semitransparent parts of the blurred background we’ve set on the .midl element. We don’t want to see the sharp edges of the .base layer text underneath, but we are because blurring causes pixels close to the edge to become semitransparent.

Screenshot. Shows two lines of blue text with a red outline to highlight the boundaries of the actual text. The text on the second line is blurred and it can be seen how this causes us to have semitransparent blue pixels on both sides of the red outline - both outside and inside.
The blur effect around the edges.

Depending on what kind of background we have on the parent of our text layers, this is a problem that can be solved with a little or a lot of effort.

If we only have a solid background, the problem gets solved by setting the background-color on our .midl element to that same value. Fortunately, this happens to be our case, so we won’t go into discussing the other scenario. Maybe in another article.

.midl {
  /* same as before */
  background: -moz-element(#blur) #fff;
  background-clip: text;
}
Firefox screenshot. Shows two text and emoji layers slightly offset from one another. The .base one, first in the DOM order, is made mono with a filter and slightly offset to the top left with a transform. The .midl one, following it in DOM order, has semitransparent text so that we can see through to the text clipped background, which uses as a background image the blurred version of the mono, slightly offset .base layer. Having explicitly set a z-index on the .midl layer and having set a fully opaque background-color on it, the .base layer now lies underneath it and it isn't visible through any semitransparent parts in the text area because there aren't any more such parts.
Firefox screenshot of the result (live demo) after ensuring the .base layer isn’t visible through the background of the .midl one.

We’re getting close to a nice result in Firefox! All that’s left to do is add the top .grey layer with the exact same styles as in the Chrome version!

.grey { filter: grayscale(1) opacity(.25); }

Sadly, doing this doesn’t produce the result we want, which is something that’s really obvious if we also make the middle layer text fully transparent (by zeroing its alpha --a: 0) so that we only see its background (which uses the blurred element #blur on top of solid white) clipped to the text area:

Firefox screenshot. Shows two text and emoji layers slightly offset from one another. The .base one, first in the DOM order, is made mono with a filter and slightly offset to the top left with a transform. The .midl one, following it in DOM order, has transparent text so that we can see through to the text clipped background, which uses as a background image the blurred version of the mono, slightly offset .base layer. Since the background-color of this layer coincides to that of their parent, it is hard to see. We also have a third .grey layer, the last in DOM order. This should be right on top of the .midl one, but, due to having set a z-index on the .midl layer, the .grey layer is underneath it and not visible, in spite of the DOM order.
Firefox screenshot of the result (live demo) after adding the top .grey layer.

The problem is we cannot see the .grey layer! Due to setting z-index: 2 on it, the middle layer .midl is now above what should be the top layer (the .grey one), in spite of the DOM order. The fix? Set z-index: 3 on the .grey layer!

.grey {
  z-index: 3;
  filter: grayscale(1) opacity(.25);
}

I’m not really fond of giving out z-index layer after layer, but hey, it’s low effort and it works! We now have a nice Firefox solution:

Firefox screenshot. Shows two text and emoji layers partly overlapping. The bottom one is mono (bluish in this case) and blurred at the intersection with the semitransparent grey one on top.
Firefox screenshot of the result (live demo) we were after.

Combining our solutions into a cross-browser one

We start with the Firefox code because there’s just more of it:

<article id='blur' class='base' aria-hidden='true'>Lion 🦁</article>
<article class='base' aria-hidden='true'>Lion 🦁</article>
<article class='midl' aria-hidden='true'>Lion 🦁</article>
<article class='grey'>Lion 🦁</article>
body { display: grid; }

article {
  grid-area: 1/ 1;
  place-self: center;
  padding: .5em;
  color: hsla(25, 100%, 55%, var(--a, 1));
  font: 900 21vw/ 1.25 cursive;
}

#blur {
  --r: 5px;
  position: absolute;
  bottom: 100vh;
}

.base {
  transform: translate(-.25em, -.25em);
  filter: sepia(1) hue-rotate(165deg) contrast(1.5) blur(var(--r, 0));
}

.midl {
  --a: 0;
  z-index: 2;
  background: -moz-element(#blur) #fff;
  background-clip: text;
}

.grey {
  z-index: 3;
  filter: grayscale(1) opacity(.25);
}

The extra z-index declarations don’t impact the result in Chrome and neither does the out-of-sight #blur element. The only things that this is missing in order for this to work in Chrome are the backdrop-filter and the mask declarations on the .midl element:

backdrop-filter: blur(5px);
-webkit-mask: linear-gradient(red, red) text;

Since we don’t want the backdrop-filter to get applied in Firefox, nor do we want the background to get applied in Chrome, we use @supports:

$r: 5px;

/* same as before */

#blur {
  /* same as before */
  --r: #{$r};
}

.midl {
  --a: 0;
  z-index: 2;
  /* need to reset inside @supports so it doesn't get applied in Firefox */
  backdrop-filter: blur($r);
  /* invalid value in Firefox, not applied anyway, no need to reset */
  -webkit-mask: linear-gradient(red, red) text;
  
  @supports (background: -moz-element(#blur)) { /* for Firefox */
    background: -moz-element(#blur) #fff;
    background-clip: text;
    backdrop-filter: none;
  }
}

This gives us a cross-browser solution!

Chrome (top) and Firefox (bottom) screenshot collage of the text and emoji glassmorphism effect for comparison. The blurred backdrop seems thicker in Chrome and the emojis are obviously different, but the result is otherwise pretty similar.
Chrome (top) and Firefox (bottom) screenshots of the result (live demo) we were after.

While the result isn’t the same in the two browsers, it’s still pretty similar and good enough for me.

What about one-elementing our solution?

Sadly, that’s impossible.

First off, the Firefox solution requires us to have at least two elements since we use one (referenced by its id) as a background for another.

Second, while the first thought with the remaining three layers (which are the only ones we need for the Chrome solution anyway) is that one of them could be the actual element and the other two its pseudos, it’s not so simple in this particular case.

For the Chrome solution, each of the layers has at least one property that also irreversibly impacts any children and any pseudos it may have. For the .base and .grey layers, that’s the filter property. For the middle layer, that’s the mask property.

So while it’s not pretty to have all those elements, it looks like we don’t have a better solution if we want the glassmorphism effect to work on emojis too.

If we only want the glassmorphism effect on plain text — no emojis in the picture — this can be achieved with just two elements, out of which only one is needed for the Chrome solution. The other one is the #blur element, which we only need in Firefox.

<article id='blur'>Blood</article>
<article class='text' aria-hidden='true' data-text='Blood'></article>

We use the two pseudos of the .text element to create the base layer (with the ::before) and a combination of the other two layers (with the ::after). What helps us here is that, with emojis out of the picture, we don’t need filter: grayscale(1), but instead we can control the saturation component of the color value.

These two pseudos are stacked one on top of the other, with the bottom one (::before) offset by the same amount and having the same color as the #blur element. This color value depends on a flag, --f, that helps us control both the saturation and the alpha. For both the #blur element and the ::before pseudo (--f: 1), the saturation is 100% and the alpha is 1. For the ::after pseudo (--f: 0), the saturation is 0% and the alpha is .25.

$r: 5px;

%text { // used by #blur and both .text pseudos
  --f: 1;
  grid-area: 1/ 1; // stack pseudos, ignored for absolutely positioned #base
  padding: .5em;
  color: hsla(345, calc(var(--f)*100%), 55%, calc(.25 + .75*var(--f)));
  content: attr(data-text);
}

article { font: 900 21vw/ 1.25 cursive }

#blur {
  position: absolute;
  bottom: 100vh;
  filter: blur($r);
}

#blur, .text::before {
  transform: translate(-.125em, -.125em);
  @extend %text;
}

.text {
  display: grid;
	
  &::after {
    --f: 0;
    @extend %text;
    z-index: 2;
    backdrop-filter: blur($r);
    -webkit-mask: linear-gradient(red, red) text;

    @supports (background: -moz-element(#blur)) {
      background: -moz-element(#blur) #fff;
      background-clip: text;
      backdrop-filter: none;
    }
  }
}

Applying the cross-browser solution to our use case

The good news here is our particular use case where we only have the glassmorphism effect on the link icon (not on the entire link including the text) actually simplifies things a tiny little bit.

We use the following Pug to generate the structure:

- let data = {
-   home: { ico: '🏠', hue: 200 }, 
-   notes: { ico: '🗒️', hue: 260 }, 
-   activity: { ico: '🔔', hue: 320 }, 
-   discovery: { ico: '🧭', hue: 30 }
- };
- let e = Object.entries(data);
- let n = e.length;

nav
  - for(let i = 0; i < n; i++)
    - let ico = e[i][1].ico;
    a.item(href='#' style=`--hue: ${e[i][1].hue}deg`)
      span.icon.tint(id=`blur${i}` aria-hidden='true') #{ico}
      span.icon.tint(aria-hidden='true') #{ico}
      span.icon.midl(aria-hidden='true' style=`background-image: -moz-element(#blur${i})`) #{ico}
      span.icon.grey(aria-hidden='true') #{ico}
      | #{e[i][0]}

Which produces an HTML structure like the one below:

<nav>
  <a class='item' href='#' style='--hue: 200deg'>
    <span class='icon tint' id='blur0' aria-hidden='true'>🏠</span>
    <span class='icon tint' aria-hidden='true'>🏠</span>
    <span class='icon midl' aria-hidden='true' style='background-image: -moz-element(#blur0)'>🏠</span>
    <span class='icon grey' aria-hidden='true'>🏠</span>
    home
  </a>
  <!-- the other nav items -->
</nav>

We could probably replace a part of those spans with pseudos, but I feel it’s more consistent and easier like this, so a span sandwich it is!

One very important thing to notice is that we have a different blurred icon layer for each of the items (because each and every item has its own icon), so we set the background of the .midl element to it in the style attribute. Doing things this way allows us to avoid making any changes to the CSS file if we add or remove entries from the data object (thus changing the number of menu items).

We have almost the same layout and prettified styles we had when we first CSS-ed the nav bar. The only difference is that now we don’t have pseudos in the top cell of an item’s grid; we have the spans:

span {
  grid-area: 1/ 1; /* stack all emojis on top of one another */
  font-size: 4em; /* bump up emoji size */
}

For the emoji icon layers themselves, we also don’t need to make many changes from the cross-browser version we got a bit earlier, though there are a few lttle ones.

First off, we use the transform and filter chains we picked initially when we were using the link pseudos instead of spans. We also don’t need the color: hsla() declaration on the span layers any more since, given that we only have emojis here, it’s only the alpha channel that matters. The default, which is preserved for the .base and .grey layers, is 1. So, instead of setting a color value where only the alpha, --a, channel matters and we change that to 0 on the .midl layer, we directly set color: transparent there. We also only need to set the background-color on the .midl element in the Firefox case as we’ve already set the background-image in the style attribute. This leads to the following adaptation of the solution:

.base { /* mono emoji version */
  transform: translate(.375em, -.25em) rotate(22.5deg);
  filter: sepia(1) hue-rotate(var(--hue)) saturate(3) blur(var(--r, 0));
}

.midl { /* middle, transparent emoji version */
  color: transparent; /* so it's not visible */
  backdrop-filter: blur(5px);
  -webkit-mask: linear-gradient(red 0 0) text;
  
  @supports (background: -moz-element(#b)) {
    background-color: #fff;
    background-clip: text;
    backdrop-filter: none;
  }
}

And that’s it — we have a nice icon glassmorphism effect for this nav bar!

Chrome (top) and Firefox (bottom) screenshot collage of the emoji glassmorphism effect for comparison. The emojis are obviously different, but the result is otherwise pretty similar.
Chrome (top) and Firefox (bottom) screenshots of the desired emoji glassmorphism effect (live demo).

There’s just one more thing to take care of — we don’t want this effect at all times; only on :hover or :focus states. So, we’re going to use a flag, --hl, which is 0 in the normal state, and 1 in the :hover or :focus state in order to control the opacity and transform values of the .base spans. This is a technique I’ve detailed in an earlier article.

$t: .3s;

a {
  /* same as before */
  --hl: 0;
  color: hsl(var(--hue), calc(var(--hl)*100%), 65%);
  transition: color $t;
  
  &:hover, &:focus { --hl: 1; }
}

.base {
  transform: 
    translate(calc(var(--hl)*.375em), calc(var(--hl)*-.25em)) 
    rotate(calc(var(--hl)*22.5deg));
  opacity: var(--hl);
  transition: transform $t, opacity $t;
}

The result can be seen in the interactive demo below when the icons are hovered or focused.

What about using SVG icons?

I naturally asked myself this question after all it took to get the CSS emoji version working. Wouldn’t the plain SVG way make more sense than a span sandwich, and wouldn’t it be simpler? Well, while it does make more sense, especially since we don’t have emojis for everything, it’s sadly not less code and it’s not any simpler either.

But we’ll get into details about that in another article!


Icon Glassmorphism Effect in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/icon-glassmorphism-effect-in-css/feed/ 6 322098
Grainy Gradients https://css-tricks.com/grainy-gradients/ https://css-tricks.com/grainy-gradients/#comments Mon, 13 Sep 2021 19:04:51 +0000 https://css-tricks.com/?p=351302 Browse through Dribbble or Behance, and you’ll find designers using a simple technique to add texture to an image: noise. Adding noise makes otherwise solid colors or smooth gradients, such as shadows, more realistic. But despite designers’ affinity for texture, …


Grainy Gradients originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Browse through Dribbble or Behance, and you’ll find designers using a simple technique to add texture to an image: noise. Adding noise makes otherwise solid colors or smooth gradients, such as shadows, more realistic. But despite designers’ affinity for texture, noise is rarely used in web design.

In this article, we’ll generate colorful noise to add texture to a gradient with only a small amount of CSS and SVG. Alright, let’s make some noise!

Illustration by Hank Washington on Behance
Illustration by Jordan Kay on Dribbble

Interactive playground

Check it out here. The quickest way to understand what’s happening is to play with the parameters that make up the layers.

The trick: SVG noise and CSS gradients

The core technique in this article is built on top of a Stack Overflow answer by Chris Pachl to the question: Can you add noise to a CSS gradient?

The trick is to use an SVG filter to create the noise, then apply that noise as a background. Layer it underneath a gradient, boost the brightness and contrast, and that’s it — you have gradient that gradually dithers away.

The key ingredients

Here’s what we’re working with under the hood:

  • SVG turbulence: This is our noise filter.
  • Background with gradient and SVG: Next, we drop that filter into CSS as a background image that combines the filter with a CSS gradient.
  • Boost brightness and contrast: Then we turn to CSS filter to increase the brightness and contrast of the noise.
  • Blend gradients: Finally, we optionally use mix-blend-mode to further filter out colors and blend gradients together.

Let’s go into detail on each of these parts.

Using SVG turbulence

Within the realm of SVG, we can define filters, and one such filter lets us create Perlin noise. It’s called <feTurbulence> and we can define attributes, such as whether it is “turbulence” or “noise” and how fine or coarse it is. Bence Szabó explains it in much more detail as he demonstrates how it can be used to create patterns.

<svg viewBox="0 0 200 200" xmlns='http://www.w3.org/2000/svg'>
  <filter id='noiseFilter'>
    <feTurbulence 
      type='fractalNoise' 
      baseFrequency='0.65' 
      numOctaves='3' 
      stitchTiles='stitch' />
  </filter>

  <rect width='100%' height='100%' filter='url(#noiseFilter)' />
</svg>

This SVG example creates a filter and renders a <rect> element that we can use for our grainy gradients. Notice that the SVG <filter> is defined separately from the <rect>, and the <rect> simply references it.

Play around with changing some of the properties of <feTurbulence>

We’re going to save this SVG as a separate file. We reference an external link to grab the SVG in the demos throughout in this article. In practice, though, you would reference a local file or your own CDN. It doesn’t work to reference the SVG by its id in CSS, for some quirky reason, but you can inline the SVG, as we show in the playground demo. We don’t do this in the demos for legibility reasons.

Creating a CSS background with SVG and a gradient

After we have the SVG file stored somewhere we can reference it by a URL or path, we can now use it in a CSS background, combined with a gradient.

.noise {
  /* ... */
  background:
    linear-gradient(to right, blue, transparent),
    url(https://grainy-gradients.vercel.app/noise.svg);
}

Order matters here. In this particular example, we want a solid color (i.e. no noise) to transition into noise and then into another color. We also want one end of the gradient to be transparent so that the noise shows through.

Like this:

However, this isn’t particularly nice because the noise is too muddled. We need to fray it and make it grainier. We can do that by…

Boosting the brightness and contrast

Adding a CSS filter makes the noise more stark, pushing the most faded colors towards white or black. The filter applies to the entire <div>, so the leftmost blue is a different blue than the pure blue we started with.

.noise {
  /* ... */
  background: 
    linear-gradient(to right, blue, transparent), 
    url(https://grainy-gradients.vercel.app/noise.svg);
  filter: contrast(170%) brightness(1000%);  
}

You can play around with how contrast and brightness affect the gradient. Boosting the brightness and contrast pushes out the muddled grays in the follow demo.

The noise is not uniform in color

If you zoom in, you’ll notice that the noise is made up of many colors. The SVG filter was colorful to begin with, and increasing the brightness and contrast emphasized certain colors. Although hardly noticeable, if this confetti is unwelcome, we can continue to filter out colors with CSS blending (i.e. mix-blend-mode and background-blend-mode ).

CSS blending

Let’s make a grainy gradient that transitions between two colors. CSS blending allows us to stack layers of color. In the next example, we’re adding another <div> to the markup, positioning it over the original gradient, then applying mix-blend-mode: multiply; to smooth things out.

<section>
  <div class="isolate">
    <div class="noise"></div>
    <div class="overlay"></div>
  </div>
</section>
.noise {
  /* ... */
  background: 
    linear-gradient(20deg, rebeccapurple, transparent), 
    url(https://grainy-gradients.vercel.app/noise.svg); 
  contrast(170%) brightness(1000%);
}
.overlay {
  /* ... */
  background: moccasin;
  mix-blend-mode: multiply;
}

We can use the CSS isolation property to create a new stacking context and choose what gets blended. If we were to leave out isolation in the next example, the gradient and overlay would blend with the background color. Try it in the Pen and comment out that line!

/* Same as before */

.isolate {
  isolation: isolate;
  /* ... */
}

Some use cases

We’ve looked at a pretty simple example of how to make a noisy gradient, but where might you use one? Let’s consider several use cases.

Light and shadows, with grain

Where do gradients naturally occur? Light and shadows, for one. We can take advantage of the CSS property mix-blend-mode to smoothly blend gradients and selectively filter the colors we want to see in the noise.

In the “shadow” example, we create a dark gradient, and invert it to create the effect in the “light” example. In both cases, mix-blend-mode allows us to blend it with other gradients.

Holographic foil

The drastic brightness and contrast boost creates a rainbow effect that’s reminiscent of holographic foil.

Taking things further

Try the playground and mess around with the different parameters to see how they affect the texture.

Beyond that, here are some ways to continue fiddling with this technique:

  • Use a different SVG: All of the gradients in this article use the same SVG, but you can toy with the parameters that generates the noise to adjust the coarseness as well as the look and feel in the playground.
  • Try different gradients: Besides linear-gradient, CSS offers four more types of gradients. Can you name them? (Here’s one.)
  • Add more layers: With CSS blending, you can stack as many any layers as you’d like and blend them down.
  • Apply different SVG filters: There are all kinds of filters, including Gaussian blur and different types of lighting. Plus, they can be referenced in a CSS filter and applied to any element in addition to SVG.

What else can you think of? Please let us know what you discover in the comments.

Browser support

We can’t escape talking about browser support here. The core of this technique is supported by all modern browsers. As you might expect, it does not work in Internet Explorer. That said, Internet Explorer does support SVG as a background in CSS (just not with the actual CSS filter property).

SVG as a CSS background image

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

ChromeFirefoxIEEdgeSafari
5249165

Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
10810734.2-4.3

CSS filter effects

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

ChromeFirefoxIEEdgeSafari
18*35No796*

Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
1081074.4*6.0-6.1*

I’ve also noticed that Blink-based browsers (e.g. Chrome) and WebKit-based one (e.g. Safari) implement mix-blend-mode slightly differently, so please be sure to test across browsers if using CSS blending. In my own projects, I’ve used browser-specific media queries to manually reconcile the visual differences with small tweaks to CSS.


That’s it! Now that you have a grasp of SVG filters and how to combine them with CSS filters as a background, you have yet another neat visual effect to add depth and texture to a design.


Grainy Gradients originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/grainy-gradients/feed/ 9 351302
Adding Shadows to SVG Icons With CSS and SVG Filters https://css-tricks.com/adding-shadows-to-svg-icons-with-css-and-svg-filters/ https://css-tricks.com/adding-shadows-to-svg-icons-with-css-and-svg-filters/#comments Fri, 11 Jun 2021 14:30:29 +0000 https://css-tricks.com/?p=342164 Why would we need to apply shadows to SVG?

  1. Shadows are a common design feature that can help elements, like icons, stand out. They could be persistent, or applied in different states (e.g. :hover, :focus, or :active)


Adding Shadows to SVG Icons With CSS and SVG Filters originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Why would we need to apply shadows to SVG?

  1. Shadows are a common design feature that can help elements, like icons, stand out. They could be persistent, or applied in different states (e.g. :hover, :focus, or :active) to indicate interaction to users.
  2. Shadows happen in real life, so they can be used on screens to breathe some life into your elements and add a touch of realism to a design.

Since we’re making lists, there are two primary ways we can apply shadows to an SVG:

  1. Using the CSS filter() property
  2. Using an SVG <filter>

Yes, both involve filters! And, yes, both CSS and SVG have their own types of filters. But there is some crossover between these as well. For example, a CSS filter can refer to an SVG <filter>; that is, if we’re working with an inline SVG instead of, say, an SVG used as a background image in CSS.

What you can’t use: the CSS box-shadow property. This is commonly used for shadows, but it follows the rectangular outside edge of elements, not the edges of the SVG elements like we want. Here’s Michelle Barker with a clear explanation:

Two flat kitten faces in bright pink showing ears eyes and whiskers. The first kitten has a drop shadow around its box and the second kitten has a drop shadow around its path edges.

If you’re using an SVG icon font, though, there is always text-shadow. That will indeed work. But let’s focus on those first two as they’re in line with a majority of use cases.

Shadows with CSS filters

The trick to applying a shadow directly to SVG via CSS filters is the drop-shadow() function :

svg {
  filter: drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));
}

That will apply a shadow that starts at 3px horizontally, 5px down, with 2px of blur, and is 40% black. Here are some examples of that:

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

ChromeFirefoxIEEdgeSafari
18*35No796*

Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
1081074.4*6.0-6.1*

Call an SVG filter inside a CSS filter

Say we have an SVG filter in the HTML:

<svg height="0" width="0">
  
  <filter id='shadow' color-interpolation-filters="sRGB">
    <feDropShadow dx="2" dy="2" stdDeviation="3" flood-opacity="0.5"/>
  </filter>
  
</svg>

We can use a CSS filter to call that SVG filter by ID instead of values we saw earlier:

svg {
  filter: url(#shadow);
}

Now that filter is taken from the HTML and referenced in the CSS, which applies it.

Using SVG filter primitives

You might be wondering how we got that SVG <filter> to work. To make a drop shadow with an SVG filter, we make use of a filter primitive. A filter primitive in SVG is an element that takes some sort of image or graphic as an input, then outputs that image or graphic it when it’s called. They sort of work like filters in a graphic editing application, but in code and can only be used inside an SVG <filter> element.

There are lots of different filter primitives in SVG. The one we’re reaching for is <feDropShadow>. I’ll let you guess what to does just by looking at the name.

So, similar to how we had something like this did this with a CSS filter:

svg {
  filter: drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));
}

…we can accomplish the same with the <feDropShadow> SVG filter primitive. There are three key attributes worth calling out as they help define the appearance of the drop shadow:

  • dx — This shifts the position of the shadow along the x-axis.
  • dy — This shifts the position of the shadow along the y-axis.
  • stdDeviation — This defines the standard deviation for the drop shadow’s blur operation. There are other attributes we can use, such as the flood-color for setting the drop shadow color, and flood-opacity for setting the drop shadow’s opacity.

That example includes three <filter> elements, each with their own <feDropShadow> filter primitives.

Using SVG filters

SVG filters are very powerful. We just looked at <feDropShadow>, which is very useful of course, but there is so much more they can do (including Photoshop-like effects) and the subset of stuff we get just for shadows is extensive. Let’s look at some, like colored shadows and inset shadows.

Let’s take the SVG markup for the Twitter logo as an example :

<svg class="svg-icon" viewBox="0 0 20 20">
  <path fill="#4691f6" d="M18.258,3.266c-0.693,0.405-1.46,0.698-2.277,0.857c-0.653-0.686-1.586-1.115-2.618-1.115c-1.98,0-3.586,1.581-3.586,3.53c0,0.276,0.031,0.545,0.092,0.805C6.888,7.195,4.245,5.79,2.476,3.654C2.167,4.176,1.99,4.781,1.99,5.429c0,1.224,0.633,2.305,1.596,2.938C2.999,8.349,2.445,8.19,1.961,7.925C1.96,7.94,1.96,7.954,1.96,7.97c0,1.71,1.237,3.138,2.877,3.462c-0.301,0.08-0.617,0.123-0.945,0.123c-0.23,0-0.456-0.021-0.674-0.062c0.456,1.402,1.781,2.422,3.35,2.451c-1.228,0.947-2.773,1.512-4.454,1.512c-0.291,0-0.575-0.016-0.855-0.049c1.588,1,3.473,1.586,5.498,1.586c6.598,0,10.205-5.379,10.205-10.045c0-0.153-0.003-0.305-0.01-0.456c0.7-0.499,1.308-1.12,1.789-1.827c-0.644,0.28-1.334,0.469-2.06,0.555C17.422,4.782,17.99,4.091,18.258,3.266" ></path>
</svg>

We’re going to need a <filter> element to do these effects. This needs to be within an <svg> element in the HTML. A <filter> element is never rendered directly in the browser — it is only used as something that can be referenced via the filter attribute in SVG, or the url() function in CSS.

Here is the syntax showing an SVG filter and applying it to a source image :

<svg width="300" height="300" viewBox="0 0 300 300">

  <filter id="myfilters">
    <!-- All filter effects/primitives go in here -->
  </filter>

  <g filter="url(#myfilters)">
    <!-- Filter applies to everything in this group -->
    <path fill="..." d="..." ></path>
  </g>

</svg>

The filter element is meant to hold filter primitives as children. It is a container to a series of filter operations that are combined to create a filter effects.

These filter primitive perform a single fundamental graphical operation (e.g. blurring, moving, filling, combining, or distorting) on one or more inputs. They are like building blocks where each SVG filter can be used to in conjunction with others to create an effect. <feGaussianBlur> is a popular filter primitive used to add a blur effect.

Let’s say we define the following SVG filter with <feGaussianBlur>:

<svg version="1.1" width="0" height="0">
  <filter id="gaussian-blur">
    <feGaussianBlur stdDeviation="1 0" />
  </filter>
</svg>

When applied on an element, this filter creates a Gaussian blur that blurs the element on a 1px radius on the x-axis, but no blurring on the y-axis. Here’s the result, with and without the effect:

It is possible to use multiple primitives inside a single filter. This will create interesting effects, however, you need to make the different primitives aware of each other. Bence Szabó has a crazy cool set of patterns he created this way.

When combining multiple filter primitives, the first primitive uses the original graphic (SourceGraphic) as its graphic input. Any subsequent primitive uses the result of the filter effect before it as its input. And so on. But we can get some flexibility on that with using the in, in2 and result attributes on primitive elements. Steven Bradley has an excellent write-up on filter primitives that dates back to 2016, but still hold true today.

There are 17 primitives we can use today:

  • <feGaussianBlur>
  • <feDropShadow>
  • <feMorphology>
  • <feDisplacementMap>
  • <feBlend>
  • <feColorMatrix>
  • <feConvolveMatrix>
  • <feComponentTransfer>
  • <feSpecularLighting>
  • <feDiffuseLighting>
  • <feFlood>
  • <feTurbulence>
  • <feImage>
  • <feTile>
  • <feOffset>
  • <feComposite>
  • <feMerge>

Notice the fe prefix on all of them. That stands for filter effect. Understanding SVG filters is challenging. An effect like an inset shadow requires a verbose syntax that is difficult to grasp without a thorough understanding of math and color theory. (Rob O’Leary’s “Getting Deep Into Shadows” is a good place to start.)

Rather than running down the rabbit hole of all that, we’re going to work with some pre-made filters. Fortunately, there are a lot of ready-to-use SVG filters around.

Inset shadows

To use filter effect on the Twitter logo, we need to declare it in our “SVG source document” with a unique ID for referencing in our <filter> tag.

<filter id='inset-shadow'>
  <!-- Shadow offset -->
  <feOffset
    dx='0'
    dy='0'
  />

  <!-- Shadow blur -->
  <feGaussianBlur
    stdDeviation='1'
    result='offset-blur'
  />

  <!-- Invert drop shadow to make an inset shadow -->
  <feComposite
    operator='out'
    in='SourceGraphic'
    in2='offset-blur'
    result='inverse'
  />
  
  <!-- Cut color inside shadow -->
  <feFlood
    flood-color='black'
    flood-opacity='.95'
    result='color'
  />
  <feComposite
    operator='in'
    in='color'
    in2='inverse'
    result='shadow'
  />

  <!-- Placing shadow over element -->
  <feComposite
    operator='over'
    in='shadow'
    in2='SourceGraphic'
  />
</filter>

There are four different primitives in there and each one performs a different function. But, taken together, they achieving an inset shadow.

Now that we’ve created this inset shadow filter, we can apply it to our SVG. We’ve already seen how to apply it via CSS. Something like:

.filtered {
  filter: url(#myfilters);
}

/* Or apply only in certain states, like: */
svg:hover, svg:focus {
  filter: url(#myfilters);
} 

We can also apply an SVG <filter> directly within the SVG syntax with the filter attribute. That’s like:

<svg>

  <!-- Apply a single filter -->
  <path d="..." filter="url(#myfilters)" />

  <!-- Or apply to a whole group of elements -->
  <g filter="url(#myfilters)">
    <path d="..." />
    <path d="..." />
  </g>
</svg>

More examples

Here are some more shadow examples from Oleg Solomka:

Note that the basic shadows here are probably a bit more complicated than they need to be. For example, a colored shadow can still be done with <feDropShadow> like:

<feDropShadow dx="-0.8" dy="-0.8" stdDeviation="0"
  flood-color="pink" flood-opacity="0.5"/>

But that embossed effect is pretty great as a filter!

Also note that you might see SVG filters in SVG syntax like this:

<svg height="0" width="0" style="position: absolute; margin-left: -100%;">
  <defs>
    <filter id="my-filters">
      <!-- ... -->
    </filter>

    <symbol id="my-icon">
      <!-- ... -->
    </symbol>
  </defs>
</svg>

On the first line there, that’s saying: this SVG shouldn’t render at all — it’s just stuff that we intend to use later. The <defs> tag says something similar: we’re just defining these things to use later. That way, we don’t have to repeat ourselves by writing things out over and again. We’ll reference the filter by ID, and the symbols as well, perhaps like:

<svg>
  <use xlink:href="#my-icon" />
</svg>

SVG filters have wide support (even in Internet Explorer and Edge!) with very fast performance.

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

ChromeFirefoxIEEdgeSafari
8310126

Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
1081074.46.0-6.1

Wrapping things up

A final comparison:

  • CSS filters are easier to use, but are much more limited. I don’t think it’s possible to add an inset shadow with the drop-shadow() function, for example.
  • SVG filters are much more robust, but much more complicated as well, and require having the <filter> somewhere in the HTML.
  • They both have great browser support and perform well on all modern browsers, though SVG filters have (surprisingly) the deepest browser support.

In this article, we have seen why and how to apply shadow to SVG icons with examples on each. Have you done this, but did it a different way than anything we looked at? Have you tried to do a shadow effect that you found impossible to pull off? Please share!


Adding Shadows to SVG Icons With CSS and SVG Filters originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/adding-shadows-to-svg-icons-with-css-and-svg-filters/feed/ 11 342164
Getting Deep Into Shadows https://css-tricks.com/getting-deep-into-shadows/ https://css-tricks.com/getting-deep-into-shadows/#comments Mon, 22 Feb 2021 16:00:16 +0000 https://css-tricks.com/?p=334324 Let’s talk shadows in web design. Shadows add texture, perspective, and emphasize the dimensions of objects. In web design, using light and shadow can add physical realism and can be used to make rich, tactile interfaces.

Take the landing page …


Getting Deep Into Shadows originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Let’s talk shadows in web design. Shadows add texture, perspective, and emphasize the dimensions of objects. In web design, using light and shadow can add physical realism and can be used to make rich, tactile interfaces.

Take the landing page below. It is for cycling tours in Iceland. Notice the embellished drop shadow of the cyclist and how it creates the perception that they are flying above not only the content on the page, but the page itself, as though they are “popping” over the screen. It feels dynamic and immediate, which is perfect for the theme of adventure.

A view of a BMX biker from above leaping over a vast area of light brown land with the words free ride along the bottom edge in large, bold, and white block lettering.
Credit: Kate Hahu

Compare that with this next example. It’s a “flat” design, sans shadows. In this case, the bike itself is the focal point. The absence of depth and realism allows the bike to stand out on its own.

Screenshot of a webpage with a light pink background with a white box that contains the site content with a headline that reads "Ride as Much or as Little" in red, an email subscription form, and a large image of a red and white bicycle to the right.
Credit: saravana

You can appreciate the differences between these approaches. Using shadows and depth is a design choice; they should support the theme and the message you want the content to convey.

Light and shadows

As we just saw, depth can enhance content. And what exactly makes a shadow? Light!

It’s impossible to talk about shadow without getting into light. It controls the direction of a shadow as well as how deep or shallow the shadow appears. You can’t have one without the other.

Google’s Material Design design system is a good example of employing light and shadows effectively. You’ve certainly encountered Material Design’s aesthetics because Google employs it on nearly all of its products.

A masonry grid of photos on a mobile screen. The purple header that contains the page title and hamburger menu has a shadow along its bottom edge that separates it from the white background of the photos.
White modal with a list of checkboxes. The modal has an underlying black shadow against the background.
A two-by-three grid of cards on a mobile screen. Each card has a light shallow shadow against a white background.

The design system takes cues from the physical world and expresses interfaces in three-dimensional space using light, surfaces, and cast shadows. Their guidelines on using light and shadows covers this in great detail.

In the Material Design environment, virtual lights illuminate the UI. Key lights create sharper, directional shadows, called key shadows. Ambient light appears from all angles to create diffused, soft shadows, called ambient shadows.

Shadows are a core component of Material Design. Compare that with Apple’s Human Interface Guidelines for macOS, where translucency and blurring is more of a driving factor for evoking depth.

Screenshot of Apple's Reminders app on a desktop. The left column that contains search and navigation is opaque and blends lightly into the desktop background while the solid white right column contains a checkbox list of reminders.
Screenshot of Apple's Maps app. The left column contains the map addresses and different route options with an opaque background that lightly blends in with the desktop background. The right column contains the map and does not blend in with the background.

In this case, light is still an influential factor, as it allows elements to either blend into the desktop, or even into other panels in the UI. Again, it’s is a design choice to employ this in your interface. Either way, you can see how light influences the visual perception of depth.

Light sources and color

Now that we understand the relationship between light and shadows, we ought to dig in a little deeper to see how light affects shadows. We’ve already seen how the strength of light produces shadows at different depths. But there’s a lot to say about the way light affects the direction and color of shadows.

There are two kinds of shadows that occur when a light shines on an object, a drop shadow and a form shadow.

Photo of an orange with light shining on it from the top right. That area is brighter than the left side which is covered in shadow. The ground contains a light reflection of the orange.

Drop shadows

A drop shadow is cast when an object blocks a light source. A drop shadow can vary in tone and value. Color terminology can be dense and confusing, so let’s talk about tone and value for a moment.

Tone is a hue blended with grey. Value describes the overall lightness or darkness of a color. Value is a big deal in painting as it is how the artist translates light and object relationships to color.

Illustration showing the effects of Hue, Tint, Tone, and Shade on red rectangles. Each rectangle is a slightly different shade of red where tint adds white, tone adds gray and shade adds black.

In the web design world, these facets of color are intrinsic to the color picker UI.

Form shadows

A form shadow, on the other hand, is the side of an object facing away from the light source. A form shadow has softer, less defined edges than a drop shadow. Form shadows illustrate the volume and depth of an object.

The appearance of a shadow depends on the direction of light, the intensity of light, and the distance between the object and the surface where the shadow is cast. The stronger the light, the darker and sharper the shadow is. The softer the light, the fainter and softer the shadow is. In some cases, we get two distinct shadows for directional light. The umbra is where light is obstructed and penumbra is where light is cast off.

Two vertically stacked illustrations.The top is a green circle with a yellow light source coming at it from the left and both umbra and penumbra shadows are cast to the right. The bottom illustration is the same green circle and light source, but with a solid black shadow cast to the right.

If a surface is close to an object, the shadow will be sharper. If a surface is further away, the shadow will be fainter. This is not some abstract scientific stuff. This is stuff we encounter every day, whether you realize it or not.

This stuff comes up in just about everything we do, even when writing with a pencil.

Light may also be reflected from sides of an object or another surface. Bright surfaces reflect light, dark surfaces absorb light.

These are the most valuable facets of light to understand for web design. The physics behind light is a complex topic, I have just lightly touched on some of it here. If you’d like to see explicit examples of what shadows are cast based on different light sources, this guide to drawing shadows for comics is instructive.

Positioning light sources

Remember, shadows go hand-in-hand with light, so defining a light source — even though there technically isn’t one — is the way to create impressive shadow effects. The trick is to consistently add shadows relative to the light source. A light source positioned above an element will cast a shadow below the element. Placing a light source to the left of an element will cast a shadow to the right. Placing multiple light sources to the top, bottom, left and right of an element actually casts no shadow at all!

Showing two browser mockups side by side. The left has light shining on it from all four directions showing uniform light and no shadows. The right has a single light source from the top casting a shadow along the bottom edge.

A light source can be projected in any direction you choose. Just make sure it’s used consistently in your design, so the shadow on one element matches other shadows on the page.

Elevation

Shadows can also convey elevation. Once again, Material Design is a good example because it demonstrates how shadows are used to create perceived separation between elements.

Showing a mobile screen flat on a light blue background with header, box, and navigational elements elevated over the screen showing depth.
Credit: Nate Wilson

Inner shadows

Speaking of elevation, the box-shadow property is the only property that can create inner shadows for a sunken effect. So, instead of elevating up, the element appears to be pressed in. That’s thanks to the inset keyword.

That good for something like an effect where clicking a button appears to physically press it.

It’s also possible to “fake” an inner text shadow with a little trickery that’s mostly supported across browsers:

Layering shadows

We’re not limited to a single shadow per element! For example, we can provide a comma-separated list of shadows on the box-shadow property. Why would we want to do that? Smoother shadows, for one.

Interesting effects is another.

Layering shadows can even enhance typography using the text-shadow property.

Just know that layering shadows is a little different for filter: drop-shadow() It’s syntax also takes a list, but it’s space-separated instead of comma-separated.

.box {
  box-shadow: 
    0 2px 2px #555, /* commas */
    0 6px 5px #777,
    0 12px 10px #999
  ;
}

.box {
  filter:
    drop-shadow(0 2px 2px #555) /* spaces */
    drop-shadow(0 6px 5px #777)
    drop-shadow(0 12px 10px #999);
}

Another thing? Shadows stack on top of one another, in the order they are declared where the top shadow is the first one in the list.

Two vertically stacked examples showing a white circle with a yellow and a grey circle behind it and the CSS code snippets that create the effect. On the top, the gray shadow is above the yellow shadow. On the bottom, the yellow shadow is above the gray shadow.

You may have guessed that drop-shadow() works a little differently here. Shadows are added exponentially, i.e. 2^number of shadows - 1.

Here’s how that works:

  • 1 shadow = (2^1 – 1). One shadow is rendered.
  • 2 shadows = (2^2 – 1). Three shadows are rendered.
  • 3 shadows = (2^3 – 1). Seven shadows are rendered.

Or, in code:

.one-shadow {
  filter: drop-shadow(20px 20px 0 grey);
}

.three-shadows {
  filter: 
    drop-shadow(20px 20px 0 grey)
    drop-shadow(40px 0 0 yellow);
}

.seven-shadows {
  filter: 
    drop-shadow(20px 20px 0 grey)
    drop-shadow(40px 0 0 yellow);
    drop-shadow(80px 0 0 red);
}

The <feDropShadow> element works the exact same way for SVGs.

Shadows and accessibility

Here’s something for you to chew on: shadows can help improve accessibility.

Google conducted a study with low-vision participants to better understand how shadows and outlines impact an individual’s ability to identify and interact with a component. They found that using shadows and outlines:

  • increases the ease and speed of finding a component when scanning pages, and
  • improves one’s ability to determine whether or not a component is interactive.

That wasn’t a wide-ranging scientific study or anything, so let’s turn around and see what the W3C says in it’s guidelines for WCAG 2.0 standards:

[…] the designer might darken the background behind the letter, or add a thin black outline (at least one pixel wide) around the letter in order to keep the contrast ratio between the letter and the background above 4.5:1.

That’s talking about light text on a light background. WCAG recommends a contrast ratio that’s at least 4.5:1 between text and images. You can use text shadows to add a stronger contrast between them.

Photo credit: Woody Van der Straeten

Shadows and performance

Before diving into shadows and adding them on all the things, it’s worth calling out that they do affect performance.

For example, filter: drop-shadow is hardware-accelerated by some browsers. A new compositor layer may be created for that element, and offloaded to the GPU. You don’t want to have too many layers, as it takes up limited GPU memory, and will eventually degrade performance. You can assess this in your browser’s DevTools.

Blurring is an expensive operation, so use it sparingly. When you blur something, it mixes the colors from pixels all around the output pixel to generate a blurred result. For example, if your <blur-radius> parameter is 2px, then the filter needs to look at two pixels in every direction around each output pixel to generate the mixed color. This happens for each output pixel, so that means a lot of calculations that grow exponentially. So, shadows with a large blur radius are generally slower to render than other shadows.

Did you know?

Did you know that shadows don’t influence the document layout?

A shadow is the same size as the element it targets. You can modify the size of a box-shadow (through the spread radius parameter), but other properties cannot modify the shadow size.

And did you know that a shadow implicitly has a lower z-index than elements? That’s why shadows sit below other elements.

And what about clipping and masking? If an element with a box-shadow is clipped (with clip-path) or uses a mask (with mask), the shadow isn’t shown. Conversely, if an element with text-shadow or filter: drop-shadow() is clipped, a shadow is shown, as long as it is within the clip region.

Here’s another: We can’t create oblique shadows (with diagonal lines) with shadow properties. That requires creating a shadow element and use a transform:skew() on it.

Oh, and one more: box-shadow follows border-radius. If an element has rounded corners, the shadow is rounded as well. In other words, the shadow mirrors the shape of the box. On the other hand, filter: drop-shadow() can create an irregular shape because it respects transparency and follows the shape of the content.

Showing two of the same card component side-by-side. They are brightly colored with a background gradient that goes from red to gold. The Nike logo is at the top, a title is below it, then a paragraph of white text beneath that. A red show with an exaggerated shadow is on both cards. The cards illustrated the difference between box shadow, which follows the boundaries of the card's edges, and drop shadow, which includes the shape of the shoe outside the card boundary.

Best use cases for different types of shadows

Practically anything on the web can have a shadow and there are multiple CSS properties and functions that create shadows. But choosing the right type of shadow is what makes a shadow effective.

Let’s evaluate the options:

  • box-shadow: This CSS property creates shadows that conform to the elements bounding box. It’s versatile and can be used on anything from cards to buttons to just about anything where the shadow simply needs to follow the element’s box.
  • text-shadow: This is a CSS property that creates shadows specifically for text elements.
  • filter: drop-shadow(): The CSS property here is filter, but what create the shadow is the drop-shadow function it accepts. What makes this type of shadow different from, say box-shadow, is that it follows the rendered shape of any element (including pseudos).
  • <feDropShadow>: This is actually an SVG element, whereas the rest are CSS properties. So, you would use this to create drop shadows directly in SVG markup.

Once you get the hang of the different types of shadows and each one’s unique shadow-creating powers, the possibilities for shadow effects feels endless. From simple drop shadows to floating elements, and even inner shadows, we can create interesting visuals that add extra meaning or value to UI.

The same goes for text shadows.

Shadows in the wild

Shadows are ubiquitous. We’re seeing them used in new and interesting ways all the time.

Have you heard the buzzword “neumorphism” floating around lately? That’s all about shadows. Here’s an implementation by Maria Muñoz:

Yuan Chuan, who makes amazing generative art, calls shadows a secret weapon in UI design:

CSS relies on existing DOM structure in the browser. It’s not possible to generate new elements other than ::before and ::after. Sometimes I really wish CSS had the ability to do so straightforwardly.

Yet, we can partially make up for this by creating various shadows and gradients entirely in CSS.

That’s why having drop-shadow is so exciting. Together with text-shadow and box-shadow we can do a lot more.

Just check out how he uses drop shadows to create intricate patterns.

Yes, that’s pretty crazy. And speaking of crazy, it’s worth mentioning that going too crazy can result in poor performance, so tread carefully.

What about pseudo-elements?

Oh yes, shadow properties are supported by the ::before and ::after pseudo-elements.

Other pseudos that respect shadows? The ::first-letter pseudo-element accepts box-shadow and text-shadow. The ::first-line pseudo-element accepts text-shadow.

Look at how Jhey Tompkins got all creative using box-shadow on pseudo elements to create animated loaders.

Animating shadows

Yes, we can make them move! The properties and function we’ve covered here are totally compatible with CSS animations and transitions. That means we can move shadows, blur shadows, expand/shrink shadows (with box-shadow), and alter the color.

Animating a shadow can provide a user with a cue that an element is interactive, or that an action has taken place. We saw earlier with our button example that an inset shadow showed that the button had been pressed. Another common animation pattern is elevating a card on hover.

If you want to optimize the animation performance, avoid animating box-shadow! It is more performant to animate drop-shadow(). But if you want the smoothest animation, a hack is the best option! Add an ::after pseudo-element with a bigger box-shadow, and animate its opacity instead.

Of course, there is a lot more you can animate. I will leave that exploration up to you!

Wrapping up

Phew, who knew there was so much to something as seemingly “simple” as CSS shadows! There’s the light source and how shadows are cast. The different types of shadows and their color. There’s using shadows for evoking depth, elevating elements and insetting them. There’s the fact that we can layer shadows on top of other shadows. And that we have a selection of CSS properties that we can use for different use cases. Then, there are the accessibility and performance implications that come with them. And, hey, animation is thing! That’s a heckuva lot!

Anyway, hopefully this broad overview gave you something new to chew on, or at the very least, helped you brush up on some concepts.


Getting Deep Into Shadows originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/getting-deep-into-shadows/feed/ 8 334324
Three Ways to Blob with CSS and SVG https://css-tricks.com/three-ways-to-blob-with-css-and-svg/ https://css-tricks.com/three-ways-to-blob-with-css-and-svg/#comments Fri, 19 Feb 2021 16:09:59 +0000 https://css-tricks.com/?p=334309 Blobs are the smooth, random, jelly-like shapes that have a whimsical quality and are just plain fun. They can be used as illustration elements and background effects on the web.

So, how are they made? Just crack open an illustration …


Three Ways to Blob with CSS and SVG originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Blobs are the smooth, random, jelly-like shapes that have a whimsical quality and are just plain fun. They can be used as illustration elements and background effects on the web.

So, how are they made? Just crack open an illustration app and go for it, right? Sure, that’s cool. But we’re in a post here on CSS-Tricks, and it would be much more fun to look at the possibilities we have to do this with CSS and SVG — two of our favorite ingredients!

We actually have a few ways to go about blobs. Let’s check them out.

Drawing circles in SVG

Let’s start easy. We can draw SVG in something like Illustrator, Sketch, Figma or whatever, but we’re going to draw in SVG code instead.

SVG makes it pretty trivial to draw a circle, thanks to the appropriately named <circle> element:

<circle cx="100" cy="100" r="40" fill="red" />

Those funky attributes? They make sense once you break them down:

  • cx defines the x-coordinate of center of circle.
  • cy defines the y-coordinate.
  • r is the radius.
  • fill is used to fill the shape with color.

That snippet creates a circle with a 40px radius with its center at 100px on the x-axis and 100px on the y-axis. The coordinates start from the upper-left corner of the parent container.

Let’s create multiple overlapping circles like this:

<svg height="300" width="300">
  <circle cx="80" cy="80" r="40" fill="red" />
  <circle cx="120" cy="80" r="40" fill="red" />
  <circle cx="150" cy="80" r="40" fill="red" />
  <circle cx="150" cy="120" r="40" fill="red" />
  <circle cx="100" cy="100" r="40" fill="red" />
</svg> 

<svg> acts as the art board where all the different shapes and figures are drawn. So, its height and width indicates the size in which the whole drawing needs to be enclosed. If some part of figure is out of bounds of the SVG’s size, then that part will be truncated.

But blobs aren’t always so perfectly… round. We can mix things up by using <ellipse> instead of <circle>:

<ellipse cx="200" cy="80" rx="100" ry="50" fill="red" />

This is nearly identical to the circle except the change in tag name and two radii values to define the horizontal (rx) and vertical (ry) radii separately. The funny thing is that we can still get a perfect circle if we want if the radii values are the same. So, in a sense, <ellipse> is a little more versatile.

And, if all you need is a circle, we could probably lean on CSS without SVG at all. Any box element can become a circle or ellipse with border-radius.

.circle {
  border-radius: 50%;
  height: 50px;
  width: 50px;
}

…but more on that later.

Freestyling with SVG paths

Thanks to SVG’s <path> tag, we can create any kind of shape. It is like drawing with a pencil or pen. You start from a point and draw lines, curves, shapes and close the loop.

There are many data parameters in path for different tasks like:

  • M – Moving to the point
  • L – Drawing line
  • C – Drawing a curve
  • Q – Bézier curve
  • Z – Closing the path

Chris has a super thorough guide that explains these parameters in great detail.

We just need the curve (C) parameter for the actual drawing. But we’ll also be moving the starting point and closing the path, so we’ll reach for the M and Z parameters as well.

A green blob with four edges that vary in size and shape.
This is a random blobby shape I put together using SVG’s <path> element.

Ready to break this down? Coordinates play a big role in <path> so what we’re about to look at will look like Google Maps data barfed inside our code. But it makes a lot more sense when we know what they’re doing.

Take this…

<svg xmlns="http://www.w3.org/2000/svg">
  <path
    fill="#24A148"
    d=""
  />
</svg>

Here, the d attribute stores the path data. It holds information containing where the drawing starts, what direction it moves, what shape it follows, and where it ends. For example:

<path d="M 10 10 C 20 20, 40 20, 50 10" stroke="black" fill="transparent"/>

It shows that our path starts from coordinates 10 10, indicated by the M that precedes them. Then, it establishes a Cubic Bézier curve (C) with two control points. Bézier curves are like handles on the both ends of a path that control the curviness between them. We have two Bézier “handles”: one for starting position (20 20) of the curve and another for ending position (40 20).

Show to end points for a black path line with light red lines extended from each endpoint indicating the amount of curve on each point.

Let’s use this knowledge to design our blob. The blob I drew is actually a bit complex, with a number of curves and control points. It doesn’t help that many of the coordinates aren’t full integers. But, still, now that we know what the <path> ‘s d parameter does and the attributes it uses to draw points along the path, it doesn’t look quite as scary.

But, hey, I get it. That’s a lot of code to not only write by hand, but get exactly right as well. I wouldn’t fault you for using something like this tool to generate the code for you.

Gooey effects with CSS and SVG filters

SVG path is complex. Right? What if I present you a way to convert many custom shapes (which you can create through divs) into gooey blobs? Here’s the idea. We’re going to create two rectangles that intersect. They’re the same color, but have a little transparency to darken where they intersect.

Then we’re going to leverage SVG’s blurring features to smudge the rectangles, creating an extra gooey blob with softer edges. The two intersecting rectangles will turn into this –

A bright red blob with four corners of varying shape and size, like in a reverse L shape.

Let’s first understand how filters work in SVG. They are declared using <filter> on HTML elements or other SVG elements, like circle.

circle {
  filter: url("#id_of_filter");
}

<filter> is basically a wrapper for the actual filter effects, that include:

  • <feGaussianBlur>
  • <feImage>
  • <feMerge>
  • <feColorMatrix>
  • Many more… Get the complete list here.

Our blob is blurred and colored, so that’s why we’re going to put <feGaussianBlur> and <feColorMatrix> to use.

<feGaussianBlur> takes multiple attributes, but we are only interested in two of them: how much blur we want and where we want it. The standard deviation (stdDeviation) and in properties align with those needs, respectively.

in accepts one of two values:

  • SourceGraphic – Blurs the entire shape
  • SourceAlpha – Blurs the alpha value, and is used to create shadow effects

After playing around a bit, here’s where I landed on the <feGaussianBlur> effect:

<feGaussianBlur in="SourceGraphic" stdDeviation="30" />

This goes right in the HTML markup with an ID that we call on the parent element of our blob:

<!-- The SVG filter -->
<svg style="position: absolute; width: 0; height: 0;">
  <filter id="goo">
    <feGaussianBlur in="SourceGraphic" stdDeviation="30" />
  </filter>
</svg>

<!-- The blob -->
<div class="hooks-main">
  <div></div>
  <div></div>
</div>

The filter doesn’t actually render, even though it’s in the markup. Instead, we reference it as a CSS filter on the blob’s parent element:

/* Blob parent element */
.hooks-main {
  position: absolute;
  width: 100%;
  height: 100%;
  filter: url("#goo&amp");
  overflow: hidden;
}

This isn’t done just yet. The blur is scattered and the element’s shape lost its boundary and color. We need a bulging effect with blur on the boundaries and a solid color to fill the shape. This is where our next SVG filter, <feColorMatrix>, comes into play.

There are two <feColorMatrix> attributes we want:

  • in – Indicates where the effect is applied, just like <feGaussianBlur>.
  • values – A matrix of four rows and five columns.

The values attribute bears a little more nuance. It holds a matrix that gets multiplied with the color and alpha values of each pixel and generates a new color value for that pixel. Mathematically speaking:

new pixel color value = ( values matrix ) × ( current pixel color value )

Let’s get a little numbers nerdy here. In that equation, values matrix is equal to:

[F-red1 F-green1 F-blue1 F-alpha1 F-constant1
 F-red2 F-green2 F-blue2 F-alpha2 F-constant2
 F-red3 F-green3 F-blue3 F-alpha3 F-constant3
 F-red4 F-green4 F-blue4 F-alpha4 F-constant4]

Here, F-red means a fraction of red in pixels, with a value ranging from 0 to 1. F-constant is some constant value to add (or subtract) from color value.

Breaking this down further… We have a color pixel with an RGBA value of rgba(214, 232, 250, 1). To convert it into a new color, we will multiply it with our values matrix.

Values MatrixColor Pixel (RGBA)New Color (RGBA)
[1 0 0 0 0
 0 1 0 0 0
 0 0 1 0 0
 0 0 0 1 0
 0 0 0 0 1]
×[214
 232
 250
 1
 1]
= [ 214x1 + 232x0 + 250x0 + 1x0 + 1x1
      214x0 + 232x1 + 250x0 + 1x0 + 1x1
      214x0 + 232x0 + 250x1 + 1x0 + 1x1
      214x0 + 232x0 + 250x0 + 1x1 + 1x1
      214x0 + 232x0 + 250x0 + 1x0 + 1x1 ]
= [214
  232
  250
 1
  1]

The pixel value didn’t change because we multiplied it by the identity matrix, but if you change the values of the matrix, then its pixel value will change too. Learn more about values matrix from MDN documentation.

In our case, these values seem to work pretty well:

<filter id="goo">
  <feGaussianBlur in="SourceGraphic" stdDeviation="30" />
  <feColorMatrix
    in="blur"
    values="1 0 0 0 0 
            0 1 0 0 0 
            0 0 1 0 0 
            0 0 0 30 -7"
  />
</filter>

I’ve added few more styles in the blob to stretch it from the corner.

Try to use these filter values in other shapes and let me know how they work out for you in the comments.

Using CSS border-radius

We teased this earlier, but now let’s get to the CSS border-radius property. It can also create blob-like shape, thanks to it’s ability to smooth out the corners of an element. This is possible because each corner radius is divided into two radii, one for each edge. That’s why we can have more shapes apart from circle and ellipse.

Each corner has two radii, one for each edge. For example, the top-left corder of a box has two radii, one for its left edge and one for its top edge.

You might be used to using border-radius as a shorthand for all four corners of an element:

.rounded {
  border-radius: 25%;
}

That’s a nice way to get uniformity for all of the corners. But blobs aren’t so uniform. We want some corners to be rounder than others to get some that looks gooey. That’s why we go for the constituent properties of border-radius, like:

.element {
  border-top-left-radius: 70% 60%;
  border-top-right-radius: 30% 40%;
  border-bottom-right-radius: 30% 60%;
  border-bottom-left-radius: 70% 40%;
}

And see how each properties takes two values? That’s one for each edge of the corner, giving us a lot of flexibility to curve an element into interesting shapes. Then we can drop in a background color, fill it up with a gradient, or even set a box-shadow on it to get a neat effect.


Three Ways to Blob with CSS and SVG originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/three-ways-to-blob-with-css-and-svg/feed/ 2 334309
SVG within CSS https://css-tricks.com/svg-within-css/ https://css-tricks.com/svg-within-css/#comments Mon, 08 Feb 2021 21:07:56 +0000 https://css-tricks.com/?p=333618 Stefan Judis has a “Today I Learned” (TIL) post explaining how SVGs filters can be inlined in CSS. The idea is that CSS has the filter property which supports some built-in functions, like grayscale(100%) and stuff like that.

But …


SVG within CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Stefan Judis has a “Today I Learned” (TIL) post explaining how SVGs filters can be inlined in CSS. The idea is that CSS has the filter property which supports some built-in functions, like grayscale(100%) and stuff like that.

But it can also point to a filter defined by SVG. So you could do filter: url(#my-custom-filter) which is in some inline <svg> as <filter id="my-custom-filter">. It’s kinda funny to have to refer out to the HTML like that. A filter is such a visual thing that it makes sense to bring it into the CSS. That looks like this:

img {
  filter: url('data:image/svg+xml,\
    <svg xmlns="http://www.w3.org/2000/svg">\
      <filter id="waves" x="-20%" y="-20%" width="140%" height="140%" filterUnits="objectBoundingBox" primitiveUnits="userSpaceOnUse" color-interpolation-filters="linearRGB">\
        <feTurbulence type="turbulence" baseFrequency="0.01 0.01" numOctaves="1" seed="1" stitchTiles="noStitch" result="turbulence" />\
        <feDisplacementMap in="SourceGraphic" in2="turbulence" scale="20" xChannelSelector="G" yChannelSelector="A" result="displacementMap" />\
      </filter>\
    </svg>#waves')
  ;
}

That’s Stefan’s turbulence filter example, something CSS alone definitely cannot do.

Look at all those backslashes (\). Makes ya wish CSS had template literals, eh? Makes me nervous that a code formatter or minifier would choke on that, but I don’t actually know, maybe it would be fine.

What’s nice is that the SVG remains fairly intact (readable and editable). So here you can edit the SVG filter in the CSS and have a play:

I also think of Yoksel’s tools. This editor for two-tone and three-tone images is so cool. I can pick up one of those filters and drop it into some CSS as well:

Filters aren’t the only kind of SVG that makes some sense to inline into CSS though. You can put SVG drawing right into CSS as well.

This works everywhere but Safari in a quick blast through modern browsers. But I think in the not-so-distant past, we needed to encode more of the special characters in the SVG to get it to work (although you didn’t have to resort to base64). Yoskel’s URL-encoder is literally just for this — I just don’t know that it’s necessary anymore.


SVG within CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/svg-within-css/feed/ 8 333618
Using a brightness() filter to generically highlight content https://css-tricks.com/using-a-brightness-filter-to-generically-highlight-content/ https://css-tricks.com/using-a-brightness-filter-to-generically-highlight-content/#comments Fri, 11 Sep 2020 22:29:34 +0000 https://css-tricks.com/?p=320864 Rick Strahl:

I can’t tell you how many times over the years I’ve implemented a custom ‘button’ like CSS implementation. Over the years I’ve used images, backgrounds, gradients, and opacity to effectively ‘highlight’ a control. All that works of


Using a brightness() filter to generically highlight content originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Rick Strahl:

I can’t tell you how many times over the years I’ve implemented a custom ‘button’ like CSS implementation. Over the years I’ve used images, backgrounds, gradients, and opacity to effectively ‘highlight’ a control. All that works of course, but the problem with most of these approaches is that one way or the other you’re hard coding a color value, image, or gradient.

You certainly have a lot more control if you specify exact colors, but if you can pull off brightening, darkening, or even a hue-shift in a way that feels cohesive on your site, it’s certainly a lot less code to maintain,

.button.specific-button {
  background: #4CAF50;
}
.button.specific-button:focus,
.button.specific-button:hover {
  background: #A5D6A7;
}

/* vs. */
.button:focus,
.button:hover {
  filter: brightness(120%);
}

/* or maybe you're super hardcore and do it everywhere */
:focus,
:hover {
  filter: brightness(120%) saturate(120%);
}

To Shared LinkPermalink on CSS-Tricks


Using a brightness() filter to generically highlight content originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/using-a-brightness-filter-to-generically-highlight-content/feed/ 2 320864
When Sass and New CSS Features Collide https://css-tricks.com/when-sass-and-new-css-features-collide/ https://css-tricks.com/when-sass-and-new-css-features-collide/#comments Mon, 29 Jun 2020 14:56:00 +0000 https://css-tricks.com/?p=312771 Recently, CSS has added a lot of new cool features such as custom properties and new functions. While these things can make our lives a lot easier, they can also end up interacting with preprocessors, like Sass, in funny …


When Sass and New CSS Features Collide originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Recently, CSS has added a lot of new cool features such as custom properties and new functions. While these things can make our lives a lot easier, they can also end up interacting with preprocessors, like Sass, in funny ways.

So this is going to be a post about the issues I’ve encountered, how I go around them, and why I still find Sass necessary these days.

The errors

If you’ve played with the new min() and max() functions, you may have ran into an error message like this when working with different units: “Incompatible units: vh and em.”

Screenshot. Shows the `Incompatible units: 'em' and 'vh'` error when trying to set `width: min(20em, 50vh)`.
An error when working with different types of units in the min()/ max() function

This is because Sass has its ownmin() function, and ignores the CSS min() function. Plus, Sass cannot perform any sort of computation using two values with units that don’t have a fixed relation between them.

For example, cm and in units have a fixed relation between them, so Sass can figure out what’s the result of min(20in, 50cm) and doesn’t throw an error when we try to use it in our code.

The same things goes for other units. Angular units, for example, all have a fixed relation between them: 1turn, 1rad or 1grad always compute to the same deg values. Same goes for 1s which is always 1000ms, 1kHz which is always 1000Hz, 1dppx which is always 96dpi, and 1in which is always 96px. This is why Sass can convert between them and mix them in computations and inside functions such as its own min() function.

But things break when these units don’t have a fixed relation between them (like the earlier case with em and vh units).

And it’s not just different units. Trying to use calc() inside min() also results in an error. If I try something like calc(20em + 7px), the error I get is, “calc(20em + 7px) is not a number for min.”

Screenshot. Shows the `'calc(20em + 7px)' is not a number for 'min'` error when trying to set `width: min(calc(20em + 7px), 50vh)`.
An error when using different unit values with calc() nested in the min()function

Another problem arises when we want to use a CSS variable or the result of a mathematical CSS function (such as calc(), min() or max()) in a CSS filter like invert().

In this case, we get told that “$color: 'var(--p, 0.85) is not a color for invert.”

Screenshot. Shows the `$color: 'var(--p, 0.85)' is not a color for 'invert'` error when trying to set `filter: invert(var(--p, .85))`.
var() in filter: invert() error

The same thing happens for grayscale(): “$color: ‘calc(.2 + var(--d, .3))‘ is not a color for grayscale.”

Screenshot. Shows the `$color: 'calc(.2 + var(--d, .3))' is not a color for 'grayscale'` error when trying to set `filter: grayscale(calc(.2 + var(--d, .3)))`.
calc() in filter: grayscale() error

opacity() causes the same issue: “$color: ‘var(--p, 0.8)‘ is not a color for opacity.”

Screenshot. Shows the `$color: 'var(--p, 0.8)' is not a color for 'opacity'` error when trying to set `filter: opacity(var(--p, 0.8))`.
var() in filter: opacity() error

However, other filter functions — including sepia(), blur(), drop-shadow(), brightness(), contrast() and hue-rotate()— all work just fine with CSS variables!

Turns out that what’s happening is similar to the min() and max() problem. Sass doesn’t have built-in sepia(), blur(), drop-shadow(), brightness(), contrast(), hue-rotate() functions, but it does have its own grayscale(), invert() and opacity() functions, and their first argument is a $color value. Since it doesn’t find that argument, it throws an error.

For the same reason, we also run into trouble when trying to use a CSS variable that lists at least two hsl()or hsla() values.

Screenshot. Shows the `wrong number of arguments (2 for 3) for 'hsl'` error when trying to set `color: hsl(9, var(--sl, 95%, 65%))`.
var() in color: hsl() error.

On the flip side, color: hsl(9, var(--sl, 95%, 65%)) is perfectly valid CSS and works just fine without Sass.

The exact same thing happens with the rgb()and rgba() functions.

Screenshot. Shows the `$color: 'var(--rgb, 128, 64, 64)' is not a color for 'rgba'` error when trying to set `color: rgba(var(--rgb, 128, 64, 64), .7)`.
var() in color: rgba() error.

Furthermore, if we import Compass and try to use a CSS variable inside a linear-gradient() or inside a radial-gradient(), we get another error, even though using variables inside conic-gradient() works just fine (that is, if the browser supports it).

Screenshot. Shows the At least two color stops are required for a linear-gradient error when trying to set background: linear-gradient(var(--c, pink), gold).
var() in background: linear-gradient() error.

This is because Compass comes with linear-gradient() and radial-gradient() functions, but has never added a conic-gradient() one.

The problems in all of these cases arise from Sass or Compass having identically-named functions and assuming those are what we intended to use in our code.

Drat!

The solution

The trick here is to remember that Sass is case-sensitive, but CSS isn’t.

That means we can write Min(20em, 50vh)and Sass won’t recognize it as its own min() function. No errors will be thrown and it’s still valid CSS that works as intended. Similarly, writing HSL()/ HSLA()/ RGB()/ RGBA() or Invert() allows us to avoid issues we looked at earlier.

As for gradients, I usually prefer linear-Gradient() and radial-Gradient() just because it’s closer to the SVG version, but using at least one capital letter in there works just fine.

But why?

Almost every time I tweet anything Sass-related, I get lectured on how it shouldn’t be used now that we have CSS variables. I thought I’d address that and explain why I disagree.

First, while I find CSS variables immensely useful and have used them for almost everything for the past three years, it’s good to keep in mind that they come with a performance cost and that tracing where something went wrong in a maze of calc() computations can be a pain with our current DevTools. I try not to overuse them to avoid getting into a territory where the downsides of using them outweigh the benefits.

Screenshot. Shows how `calc()` expressions are presented in DevTools.
Not exactly easy to figure out what’s the result of those calc() expressions.

In general, if it acts like a constant, doesn’t change element-to-element or state-to-state (in which case custom properties are definitely the way to go) or reduce the amount of compiled CSS (solving the repetition problem created by prefixes), then I’m going to use a Sass variable.

Secondly, variables have always been a pretty small portion of why I use Sass. When I started using Sass in late 2012, it was primarily for looping, a feature we still don’t have in CSS. While I’ve moved some of that looping to an HTML preprocessor (because it reduces the generated code and avoids having to modify both the HTML and the CSS later), I still use Sass loops in plenty of cases, like generating lists of values, stop lists inside gradient functions, lists of points inside a polygon function, lists of transforms, and so on.

Here’s an example. I used to generate n HTML items with a preprocessor. The choice of preprocessor matters less, but I’ll be using Pug here.

- let n = 12;

while n--
  .item

Then I would set the $n variable into the Sass (and it would have to be equal to that in the HTML) and loop up to it to generate the transforms that would position each item:

$n: 12;
$ba: 360deg/$n;
$d: 2em;

.item {
  position: absolute;
  top: 50%; left: 50%;
  margin: -.5*$d;
  width: $d; height: $d;
  /* prettifying styles */

  @for $i from 0 to $n {
    &:nth-child(#{$i + 1}) {
      transform: rotate($i*$ba) translate(2*$d) rotate(-$i*$ba);
			
      &::before { content: '#{$i}' }
    }
  }
}

However, this meant that I would have to change both the Pug and the Sass when changing the number of items, making the generated code very repetitive.

Screenshot. Shows the generated CSS, really verbose, almost completely identical transform declaration repeated for each item.
CSS generated by the above code

I have since moved to making Pug generate the indices as custom properties and then use those in the transform declaration.

- let n = 12;

body(style=`--n: ${n}`)
  - for(let i = 0; i < n; i++)
    .item(style=`--i: ${i}`)
$d: 2em;

.item {
  position: absolute;
  top: 50%;
  left: 50%;
  margin: -.5*$d;
  width: $d;
  height: $d;
  /* prettifying styles */
  --az: calc(var(--i)*1turn/var(--n));
  transform: rotate(var(--az)) translate(2*$d) rotate(calc(-1*var(--az)));
  counter-reset: i var(--i);
	
  &::before { content: counter(i) }
}

This significantly reduces the generated code.

Screenshot. Shows the generated CSS, much more compact, no having almost the exact same declaration set on every element separately.
CSS generated by the above code

However, looping in Sass is still necessary if I want to generate something like a rainbow.

@function get-rainbow($n: 12, $sat: 90%, $lum: 65%) {
  $unit: 360/$n;
  $s-list: ();
	
  @for $i from 0 through $n {
    $s-list: $s-list, hsl($i*$unit, $sat, $lum)
  }
	
  @return $s-list
}

html { background: linear-gradient(90deg, get-rainbow()) }

Sure, I could generate it as a list variable from Pug, but doing so doesn’t take advantage of the dynamic nature of CSS variables and it doesn’t reduce the amount of code that gets served to the browser, so there’s no benefit coming out of it.

Another big part of my Sass (and Compass) use is tied to built-in mathematical functions (such as trigonometric functions), which are part of the CSS spec now, but not yet implemented in any browser. Sass doesn’t come with these functions either, but Compass does and this is why I often need to use Compass.

And, sure, I could write my own such functions in Sass. I did resort to this in the beginning, before Compass supported inverse trigonometric functions. I really needed them, so I wrote my own based on the Taylor series. But Compass provides these sorts of functions nowadays and they are better and more performant than mine.

Mathematical functions are extremely important for me as I’m a technician, not an artist. The values in my CSS usually result from mathematical computations. They’re not magic numbers or something used purely for aesthetics. A example is generating lists of clip paths points that create regular or quasi-regular polygons. Think about the case where we want to create things like non-rectangular avatars or stickers.

Let’s consider a regular polygon with vertices on a circle with a radius 50% of the square element we start from. Dragging the slider in the following demo allows us to see where the points are placed for different numbers of vertices:

Putting it into Sass code, we have:

@mixin reg-poly($n: 3) {
  $ba: 360deg/$n; // base angle
  $p: (); // point coords list, initially empty
	
  @for $i from 0 to $n {
    $ca: $i*$ba; // current angle
    $x: 50%*(1 + cos($ca)); // x coord of current point
    $y: 50%*(1 + sin($ca)); // y coord of current point
    $p: $p, $x $y // add current point coords to point coords list
  }
	
  clip-path: polygon($p) // set clip-path to list of points
}

Note that here we’re also making use of looping and of things such as conditionals and modulo that are a real pain when using CSS without Sass.

A slightly more evolved version of this might involve rotating the polygon by adding the same offset angle ($oa) to the angle of each vertex. This can be seen in the following demo. This example tosses in a star mixin that works in a similar manner, except we always have an even number of vertices and every odd-indexed vertex is situated on a circle of a smaller radius ($f*50%, where $f is sub-unitary):

We can also have chubby stars like this:

Or stickers with interesting border patterns. In this particular demo, each sticker is created with a single HTML element and the border pattern is created with clip-path, looping and mathematics in Sass. Quite a bit of it, in fact.

Another example are these card backgrounds where looping, the modulo operation and exponential functions work together to generate the dithering pixel background layers:

This demo just happens to rely heavily on CSS variables as well.

Then there’s using mixins to avoid writing the exact same declarations over and over when styling things like range inputs. Different browsers use different pseudo-elements to style the components of such a control, so for every component, we have to set the styles that control its look on multiple pseudos.

Sadly, as tempting as it may be to put this in our CSS:

input::-webkit-slider-runnable-track, 
input::-moz-range-track, 
input::-ms-track { /* common styles */ }

…we cannot do it because it doesn’t work! The entire rule set is dropped if even one of the selectors isn’t recognized. And since no browser recognises all three of the above, the styles don’t get applied in any browser.

We need to have something like this if we want our styles to be applied:

input::-webkit-slider-runnable-track { /* common styles */ }
input::-moz-range-track { /* common styles */ }
input::-ms-track { /* common styles */ }

But that can mean a lot of identical styles repeated three times. And if we want to change, say, the background of the track, we need to change it in the ::-webkit-slider-runnable-track styles, in the ::-moz-range-track styles and in the ::-ms-track styles.

The only sane solution we have is to use a mixin. The styles get repeated in the compiled code because they have to be repeated there, but we don’t have to write the same thing three times anymore.

@mixin track() { /* common styles */ }

input {
  &::-webkit-slider-runnable-track { @include track }
  &::-moz-range-track { @include track }
  &::-ms-track { @include track }
}

The bottom line is: yes, Sass is still very much necessary in 2020.


When Sass and New CSS Features Collide originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/when-sass-and-new-css-features-collide/feed/ 16 312771
Filtering Data Client-Side: Comparing CSS, jQuery, and React https://css-tricks.com/filtering-data-client-side-comparing-css-jquery-and-react/ https://css-tricks.com/filtering-data-client-side-comparing-css-jquery-and-react/#comments Tue, 24 Sep 2019 19:43:18 +0000 https://css-tricks.com/?p=295956 Say you have a list of 100 names:

<ul>
  <li>Randy Hilpert</li>
  <li>Peggie Jacobi</li>
  <li>Ethelyn Nolan Sr.</li> 
  <!-- and then some -->
</ul>

…or file names, or phone numbers, or whatever. And you want to filter them client-side, meaning you aren’t …


Filtering Data Client-Side: Comparing CSS, jQuery, and React originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Say you have a list of 100 names:

<ul>
  <li>Randy Hilpert</li>
  <li>Peggie Jacobi</li>
  <li>Ethelyn Nolan Sr.</li> 
  <!-- and then some -->
</ul>

…or file names, or phone numbers, or whatever. And you want to filter them client-side, meaning you aren’t making a server-side request to search through data and return results. You just want to type “rand” and have it filter the list to include “Randy Hilpert” and “Danika Randall” because they both have that string of characters in them. Everything else isn’t included in the results.

Let’s look at how we might do that with different technologies.

CSS can sorta do it, with a little help.

CSS can’t select things based on the content they contain, but it can select on attributes and the values of those attributes. So let’s move the names into attributes as well.

<ul>
  <li data-name="Randy Hilpert">Randy Hilpert</li>
  <li data-name="Peggie Jacobi">Peggie Jacobi</li>
  <li data-name="Ethelyn Nolan Sr.">Ethelyn Nolan Sr.</li> 
  ...
</ul>

Now to filter that list for names that contain “rand”, it’s very easy:

li {
  display: none;
}
li[data-name*="rand" i] {
  display: list-item;
}

Note the i on Line 4. That means “case insensitive” which is very useful here.

To make this work dynamically with a filter <input>, we’ll need to get JavaScript involved to not only react to the filter being typed in, but generate CSS that matches what is being searched.

Say we have a <style> block sitting on the page:

<style id="cssFilter">
  /* dynamically generated CSS will be put in here */
</style>

We can watch for changes on our filter input and generate that CSS:

filterElement.addEventListener("input", e => {
  let filter = e.target.value;
  let css = filter ? `
    li {
      display: none;
    }
    li[data-name*="${filter}" i] {
      display: list-item;
    }
  ` : ``;
  window.cssFilter.innerHTML = css;
});

Note that we’re emptying out the style block when the filter is empty, so all results show.

See the Pen
Filtering Technique: CSS
by Chris Coyier (@chriscoyier)
on CodePen.

I’ll admit it’s a smidge weird to leverage CSS for this, but Tim Carry once took it way further if you’re interested in the concept.

jQuery makes it even easier.

Since we need JavaScript anyway, perhaps jQuery is an acceptable tool. There are two notable changes here:

  • jQuery can select items based on the content they contain. It has a selector API just for this. We don’t need the extra attribute anymore.
  • This keeps all the filtering to a single technology.

We still watch the input for typing, then if we have a filter term, we hide all the list items and reveal the ones that contain our filter term. Otherwise, we reveal them all again:

const listItems = $("li");

$("#filter").on("input", function() {
  let filter = $(this).val();
  if (filter) {
    listItems.hide();
    $(`li:contains('${filter}')`).show();
  } else {
    listItems.show();
  }
});

It’s takes more fiddling to make the filter case-insensitive than CSS does, but we can do it by overriding the default method:

jQuery.expr[':'].contains = function(a, i, m) {
  return jQuery(a).text().toUpperCase()
      .indexOf(m[3].toUpperCase()) >= 0;
};

See the Pen
Filtering Technique: jQuery
by Chris Coyier (@chriscoyier)
on CodePen.

React can do it with state and rendering only what it needs.

There is no one-true-way to do this in React, but I would think it’s React-y to keep the list of names as data (like an Array), map over them, and only render what you need. Changes in the input filter the data itself and React re-renders as necessary.

If we have an names = [array, of, names], we can filter it pretty easily:

filteredNames = names.filter(name => {
  return name.includes(filter);
});

This time, case sensitivity can be done like this:

filteredNames = names.filter(name => {
  return name.toUpperCase().includes(filter.toUpperCase());
});

Then we’d do the typical .map() thing in JSX to loop over our array and output the names.

See the Pen
Filtering Technique: React
by Chris Coyier (@chriscoyier)
on CodePen.

I don’t have any particular preference

This isn’t the kind of thing you choose a technology for. You do it in whatever technology you already have. I also don’t think any one approach is particularly heavier than the rest in terms of technical debt.


Filtering Data Client-Side: Comparing CSS, jQuery, and React originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/filtering-data-client-side-comparing-css-jquery-and-react/feed/ 10 295956
backdrop-filter https://css-tricks.com/almanac/properties/b/backdrop-filter/ Fri, 02 Aug 2019 14:12:47 +0000 https://css-tricks.com/?page_id=293365 The backdrop-filter property in CSS is used to apply filter effects (grayscale, contrast, blur, etc) to the background/backdrop of an element. The backdrop-filter has the same effect as the filter property, except the filter effects are …


backdrop-filter originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
The backdrop-filter property in CSS is used to apply filter effects (grayscale, contrast, blur, etc) to the background/backdrop of an element. The backdrop-filter has the same effect as the filter property, except the filter effects are applied only to the background and instead of to the element’s content.

Classic example:

Filtered backgrounds without backdrop-filter

Before backdrop-filter, the only way to add a filtered background was to add a separate “background” element, position it behind the foreground element(s) and filter it like so:

<div class="wrapper">
  <div class="background"></div>
  <div class="foreground"></div>
</div>
.background {
  filter: blur(4px);
  position: absolute;
  width: 100%;
  height: 100%;
}

The backdrop-filter property allows you to eliminate this extra “background” element and apply filters to the backdrop directly:

.foreground {
  backdrop-filter: blur(10px);
} /* No .wrapper needed! */

At the time of writing, backdrop-filter is part of the Filter Effects Module 2 Editor’s Draft and is not officially part of any specification. Browser support is currently limited (see below).

Syntax

.element {
  backdrop-filter: <filter-function> [<filter-function>]* | none
}

can be any one of the following:

  • blur()
  • brightness()
  • contrast()
  • drop-shadow()
  • grayscale()
  • hue-rotate()
  • invert()
  • opacity()
  • saturate()
  • sepia()
  • url() – (for applying SVG filters)

You can apply multiple <filter-function>s to a backdrop, like so:

.element {
  backdrop-filter: grayscale(0.5) opacity(0.8) /* ...and on and on... */;
}

See the W3C Filter Effects Module Working Draft for acceptable values for each of the filter functions.

Example

For a comprehensive look at filter functions and nifty ways to use them together, see the filter almanac entry on CSS-Tricks.

The following Pen is forked from an example in Robin Rendle’s article exploring backdrop-filter.

Browser support

This browser support data is from Caniuse, which has more detail. A number indicates that browser supports the feature at that version and up.

Desktop

ChromeFirefoxIEEdgeSafari
76103No179*

Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
1081071089.0-9.2*

Resources


backdrop-filter originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
293365
Using “box shadows” and clip-path together https://css-tricks.com/using-box-shadows-and-clip-path-together/ https://css-tricks.com/using-box-shadows-and-clip-path-together/#comments Wed, 10 Apr 2019 23:55:49 +0000 http://css-tricks.com/?p=285899 Let’s do a little step-by-step of a situation where you can’t quite do what seems to make sense, but you can still get it done with CSS trickery. In this case, it’ll be applying a shadow to a shape.

You…


Using “box shadows” and clip-path together originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Let’s do a little step-by-step of a situation where you can’t quite do what seems to make sense, but you can still get it done with CSS trickery. In this case, it’ll be applying a shadow to a shape.

You make a box

.tag {
  background: #FB8C00;
  color: #222;
  font: bold 32px system-ui;
  padding: 2rem 3rem 2rem 4rem;
}

You fashion it into a nice tag shape

You use clip-path because it’s great for that.

.tag {
  /* ... */
  clip-path: polygon(30px 0%, 100% 0%, 100% 100%, 30px 100%, 0 50%)
}

You want a shadow on it, so you…

Try to use box-shadow.

.tag {
  /* ... */
  box-shadow: 5px 5px 10px rgba(0, 0, 0, 0.5);
}

But it doesn’t work. Nothing shows up. You think you’re going crazy. You assume you have the syntax wrong. You don’t. The problem is that clip-path is cutting it off.

You can drop-shadow a parent element instead

There is a filter that does shadows as well: drop-shadow(). But you can’t use it directly on the element because the clip-path will cut it off as well. So you make a parent:

<span class="tag-wrap">
  <span class="tag">
    Tag
  </span>
</span>

You can’t use box-shadow on that parent either, because the parent is still a rectangle and the shadow will look wrong. But you can use filter, and the shadow will follow the shape.

See the Pen
Shadow on Shape
by Chris Coyier (@chriscoyier)
on CodePen.

That’s all.


Using “box shadows” and clip-path together originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/using-box-shadows-and-clip-path-together/feed/ 14 285899
An Illustrated (and Musical) Guide to Map, Reduce, and Filter Array Methods https://css-tricks.com/an-illustrated-and-musical-guide-to-map-reduce-and-filter-array-methods/ https://css-tricks.com/an-illustrated-and-musical-guide-to-map-reduce-and-filter-array-methods/#comments Tue, 26 Mar 2019 14:19:39 +0000 http://css-tricks.com/?p=284828 Map, reduce, and filter are three very useful array methods in JavaScript that give developers a ton of power in a short amount of space. Let’s jump right into how you can leverage (and remember how to use!) these super …


An Illustrated (and Musical) Guide to Map, Reduce, and Filter Array Methods originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Map, reduce, and filter are three very useful array methods in JavaScript that give developers a ton of power in a short amount of space. Let’s jump right into how you can leverage (and remember how to use!) these super handy methods.

Array.map()

Array.map() updates each individual value in a given array based on a provided transformation and returns a new array of the same size. It accepts a callback function as an argument, which it uses to apply the transform.

let newArray = oldArray.map((value, index, array) => {
  ...
});

A mnemonic to remember this is MAP: Morph Array Piece-by-Piece.

Instead of a for-each loop to go through and apply this transformation to each value, you can use a map. This works when you want to preserve each value, but update it. We’re not potentially eliminating any values (like we would with a filter), or calculating a new output (like we would use reduce for). A map lets you morph an array piece-by-piece. Let’s take a look at an example:

[1, 4, 6, 14, 32, 78].map(val => val * 10)
// the result is: [10, 40, 60, 140, 320, 780]

In the above example, we take an initial array ([1, 4, 6, 14, 32, 78]) and map each value in it to be that value times ten (val * 10). The result is a new array with each value of the original array transformed by the equation: [10, 40, 60, 140, 320, 780].

An illustration of the code examples covered in this section.

Array.filter()

Array.filter() is a very handy shortcut when we have an array of values and want to filter those values into another array, where each value in the new array is a value that passes a specific test.

This works like a search filter. We’re filtering out values that pass the parameters we provide.

For example, if we have an array of numeric values, and want to filter them to just the values that are larger than 10, we could write:

[1, 4, 6, 14, 32, 78].filter(val => val > 10)
// the result is: [14, 32, 78]

If we were to use a map method on this array, such as in the example above, we would return an array of the same length as the original with val > 10 being the “transform,” or a test in this case. We transform each of the original values to their answer if they are greater than 10. It would look like this:

[1, 4, 6, 14, 32, 78].map(val => val > 10)
// the result is: [false, false, false, true, true, true]

A filter, however, returns only the true values. So the result is smaller than the original array or the same size if all values pass a specific test.

Think about filter like a strainer-type-of-filter. Some of the mix will pass through into the result, but some will be left behind and discarded.

An illustration of a funnel with numbers going in the top and a few coming out of the bottom next to a handwritten version of the code covered in this section.

Say we have a (very small) class of four dogs in obedience school. All of the dogs had challenges throughout obedience school and took a graded final exam. We’ll represent the doggies as an array of objects, i.e.:

const students = [
  {
    name: "Boops",
    finalGrade: 80
  },
  {
    name: "Kitten",
    finalGrade: 45
  },
  {
    name: "Taco",
    finalGrade: 100
  },
  {
    name: "Lucy",
    finalGrade: 60
  }
]

If the dogs get a score higher than 70 on their final test, they get a fancy certificate; and if they don’t, they’ll need to take the course again. In order to know how many certificates to print, we need to write a method that will return the dogs with passing grades. Instead of writing out a loop to test each object in the array, we can shorten our code with filter!

const passingDogs = students.filter((student) => {
  return student.finalGrade >= 70
})

/*
passingDogs = [
  {
    name: "Boops",
    finalGrade: 80
  },
  {
    name: "Taco",
    finalGrade: 100
  }
]
*/

As you can see, Boops and Taco are good dogs (actually, all dogs are good dogs), so Boops and Taco are getting certificates of achievement for passing the course! We can write this in a single line of code with our lovely implicit returns and then remove the parenthesis from our arrow function since we have single argument:

const passingDogs = students.filter(student => student.finalGrade >= 70)

/*
passingDogs = [
  {
    name: "Boops",
    finalGrade: 80
  },
  {
    name: "Taco",
    finalGrade: 100
  }
]
*/

Array.reduce()

The reduce() method takes the input values of an array and returns a single value. This one is really interesting. Reduce accepts a callback function which consists of an accumulator (a value that accumulates each piece of the array, growing like a snowball), the value itself, and the index. It also takes a starting value as a second argument:

let finalVal = oldArray.reduce((accumulator, currentValue, currentIndex, array) => {
  ...
}), initalValue;
An illustration of a saucepan cooking ingredients next to handwritten code from the examples covered in this section.

Let’s set up a cook function and a list of ingredients:

// our list of ingredients in an array
const ingredients = ['wine', 'tomato', 'onion', 'mushroom']

// a cooking function
const cook = (ingredient) => {
    return `cooked ${ingredient}`
}

If we want to reduce the items into a sauce (pun absolutely intended), we’ll reduce them with reduce()!

const wineReduction = ingredients.reduce((sauce, item) => {
  return sauce += cook(item) + ', '
  }, '')
  
// wineReduction = "cooked wine, cooked tomato, cooked onion, cooked mushroom, "

That initial value ('' in our case) is important because if we don’t have it, we don’t cook the first item. It makes our output a little wonky, so it’s definitely something to watch out for. Here’s what I mean:

const wineReduction = ingredients.reduce((sauce, item) => {
  return sauce += cook(item) + ', '
  })
  
// wineReduction = "winecooked tomato, cooked onion, cooked mushroom, "

Finally, to make sure we don’t have any excess spaces at the end of our new string, we can pass in the index and the array to apply our transformation:

const wineReduction = ingredients.reduce((sauce, item, index, array) => {
  sauce += cook(item)
  if (index < array.length - 1) {
        sauce += ', '
        }
        return sauce
  }, '')
  
// wineReduction = "cooked wine, cooked tomato, cooked onion, cooked mushroom"

Now we can write this even more concisely (in a single line!) using ternary operators, string templates, and implicit returns:

const wineReduction = ingredients.reduce((sauce, item, index, array) => {
  return (index < array.length - 1) ? sauce += `${cook(item)}, ` : sauce += `${cook(item)}`
}, '')

// wineReduction = "cooked wine, cooked tomato, cooked onion, cooked mushroom"

A little way to remember this is to recall how you make sauce: you reduce a few ingredients down to a single item.

Sing it with me!

I wanted to end this blog post with a song, so I wrote a little ditty about array methods that might just help you to remember them:


An Illustrated (and Musical) Guide to Map, Reduce, and Filter Array Methods originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/an-illustrated-and-musical-guide-to-map-reduce-and-filter-array-methods/feed/ 15 284828
Blurred Borders in CSS https://css-tricks.com/blurred-borders-in-css/ https://css-tricks.com/blurred-borders-in-css/#comments Wed, 20 Mar 2019 20:06:46 +0000 http://css-tricks.com/?p=284773 Say we want to target an element and just visually blur the border of it. There is no simple, single built-in web platform feature we can reach for. But we can get it done with a little CSS trickery.

Here’s …


Blurred Borders in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Say we want to target an element and just visually blur the border of it. There is no simple, single built-in web platform feature we can reach for. But we can get it done with a little CSS trickery.

Here’s what we’re after:

Screenshot of an element with a background image that shows oranges on a wooden table. The border of this element is blurred.
The desired result.

Let’s see how we can code this effect, how we can enhance it with rounded corners, extend support so it works cross-browser, what the future will bring in this department and what other interesting results we can get starting from the same idea!

Coding the basic blurred border

We start with an element on which we set some dummy dimensions, a partially transparent (just slightly visible) border and a background whose size is relative to the border-box, but whose visibility we restrict to the padding-box:

$b: 1.5em; // border-width

div {
  border: solid $b rgba(#000, .2);
  height: 50vmin;
  max-width: 13em;
  max-height: 7em;
  background: url(oranges.jpg) 50%/ cover 
                border-box /* background-origin */
                padding-box /* background-clip */;
}

The box specified by background-origin is the box whose top left corner is the 0 0 point for background-position and also the box that background-size (set to cover in our case) is relative to. The box specified by background-clip is the box within whose limits the background is visible.

The initial values are padding-box for background-origin and border-box for background-clip, so we need to specify them both in this case.

If you need a more in-depth refresher on background-origin and background-clip, you can check out this detailed article on the topic.

The code above gives us the following result:

See the Pen by thebabydino (@thebabydino) on CodePen.

Next, we add an absolutely positioned pseudo-element that covers its entire parent’s border-box and is positioned behind (z-index: -1). We also make this pseudo-element inherit its parent’s border and background, then we change the border-color to transparent and the background-clip to border-box:

$b: 1.5em; // border-width

div {
  position: relative;
  /* same styles as before */
  
  &:before {
    position: absolute;
    z-index: -1;
    /* go outside padding-box by 
     * a border-width ($b) in every direction */
    top: -$b; right: -$b; bottom: -$b; left: -$b;
    border: inherit;
    border-color: transparent;
    background: inherit;
    background-clip: border-box;
    content: ''
  }
}

Now we can also see the background behind the barely visible border:

See the Pen by thebabydino (@thebabydino) on CodePen.

Alright, you may be seeing already where this is going! The next step is to blur() the pseudo-element. Since this pseudo-element is only visible only underneath the partially transparent border (the rest is covered by its parent’s padding-box-restricted background), it results the border area is the only area of the image we see blurred.

See the Pen by thebabydino (@thebabydino) on CodePen.

We’ve also brought the alpha of the element’s border-color down to .03 because we want the blurriness to be doing most of the job of highlighting where the border is.

This may look done, but there’s something I still don’t like: the edges of the pseudo-element are now blurred as well. So let’s fix that!

One convenient thing when it comes to the order browsers apply properties in is that filters are applied before clipping. While this is not what we want and makes us resort to inconvenient workarounds in a lot of other cases… right here, it proves to be really useful!

It means that, after blurring the pseudo-element, we can clip it to its border-box!

My preferred way of doing this is by setting clip-path to inset(0) because… it’s the simplest way of doing it, really! polygon(0 0, 100% 0, 100% 100%, 0 100%) would be overkill.

See the Pen by thebabydino (@thebabydino) on CodePen.

In case you’re wondering why not set the clip-path on the actual element instead of setting it on the :before pseudo-element, this is because setting clip-path on the element would make it a stacking context. This would force all its child elements (and consequently, its blurred :before pseudo-element as well) to be contained within it and, therefore, in front of its background. And then no nuclear z-index or !important could change that.

We can prettify this by adding some text with a nicer font, a box-shadow and some layout properties.

What if we have rounded corners?

The best thing about using inset() instead of polygon() for the clip-path is that inset() can also accommodate for any border-radius we may want!

And when I say any border-radius, I mean it! Check this out!

div {
  --r: 15% 75px 35vh 13vw/ 3em 5rem 29vmin 12.5vmax;
  border-radius: var(--r);
  /* same styles as before */
  
  &:before {
    /* same styles as before */
    border-radius: inherit;
    clip-path: inset(0 round var(--r));
  }
}

It works like a charm!

See the Pen by thebabydino (@thebabydino) on CodePen.

Extending support

Some mobile browsers still need the -webkit- prefix for both filter and clip-path, so be sure to include those versions too. Note that they are included in the CodePen demos embeded here, even though I chose to skip them in the code presented in the body of this article.

Alright, but what if we need to support Edge? clip-path doesn’t work in Edge, but filter does, which means we do get the blurred border, but no sharp cut limits.

Well, if we don’t need corner rounding, we can use the deprecated clip property as a fallback. This means adding the following line right before the clip-path ones:

clip: rect(0 100% 100% 0)

And our demo now works in Edge… sort of! The right, bottom and left edges are cut sharply, but the top one still remains blurred (only in the Debug mode of the Pen, all seems fine for the iframe in the Editor View). And opening DevTools or right clicking in the Edge window or clicking anywhere outside this window makes the effect of this property vanish. Bug of the month right there!

Alright, since this is so unreliable and it doesn’t even help us if we want rounded corners, let’s try another approach!

This is a bit like scratching behind the left ear with the right foot (or the other way around, depending on which side is your more flexible one), but it’s the only way I can think of to make it work in Edge.

Some of you may have already been screaming at the screen something like “but Ana… overflow: hidden!” and yes, that’s what we’re going for now. I’ve avoided it initially because of the way it works: it cuts out all descendant content outside the padding-box. Not outside the border-box, as we’ve done by clipping!

This means we need to ditch the real border and emulate it with padding, which I’m not exactly delighted about because it can lead to more complications, but let’s take it one step at a time!

As far as code changes are concerned, the first thing we do is remove all border-related properties and set the border-width value as the padding. We then set overflow: hidden and restrict the background of the actual element to the content-box. Finally, we reset the pseudo-element’s background-clip to the padding-box value and zero its offsets.

$fake-b: 1.5em; // fake border-width

div {
  /* same styles as before */
  overflow: hidden;
  padding: $fake-b;
  background: url(oranges.jpg) 50%/ cover 
                padding-box /* background-origin */
                content-box /* background-clip */;
  
  &:before {
    /* same styles as before */
    top: 0; right: 0; bottom: 0; left: 0;
    background: inherit;
    background-clip: padding-box;
  }
}

See the Pen by thebabydino (@thebabydino) on CodePen.

If we want that barely visible “border” overlay, we need another background layer on the actual element:

$fake-b: 1.5em; // fake border-width
$c: rgba(#000, .03);

div {
  /* same styles as before */
  overflow: hidden;
  padding: $fake-b;
  --img: url(oranges.jpg) 50%/ cover;
  background: var(--img)
                padding-box /* background-origin */
                content-box /* background-clip */,  
              linear-gradient($c, $c);
  
  &:before {
    /* same styles as before */
    top: 0; right: 0; bottom: 0; left: 0;
    background: var(--img);
  }
}

See the Pen by thebabydino (@thebabydino) on CodePen.

We can also add rounded corners with no hassle:

See the Pen by thebabydino (@thebabydino) on CodePen.

So why didn’t we do this from the very beginning?!

Remember when I said a bit earlier that not using an actual border can complicate things later on?

Well, let’s say we want to have some text. With the first method, using an actual border and clip-path, all it takes to prevent the text content from touching the blurred border is adding a padding (of let’s say 1em) on our element.

See the Pen by thebabydino (@thebabydino) on CodePen.

But with the overflow: hidden method, we’ve already used the padding property to create the blurred “border”. Increasing its value doesn’t help because it only increases the fake border’s width.

We could add the text into a child element. Or we could also use the :after pseudo-element!

The way this works is pretty similar to the first method, with the :after replacing the actual element. The difference is we clip the blurred edges with overflow: hidden instead of clip-path: inset(0) and the padding on the actual element is the pseudos’ border-width ($b) plus whatever padding value we want:

$b: 1.5em; // border-width

div {
  overflow: hidden;
  position: relative;
  padding: calc(1em + #{$b});
  /* prettifying styles */
	
  &:before, &:after {
    position: absolute;
    z-index: -1; /* put them *behind* parent */
    /* zero all offsets */
    top: 0; right: 0; bottom: 0; left: 0;
    border: solid $b rgba(#000, .03);
    background: url(oranges.jpg) 50%/ cover 
                  border-box /* background-origin */
                  padding-box /* background-clip */;
    content: ''
  }
	
  &:before {
    border-color: transparent;
    background-clip: border-box;
    filter: blur(9px);
  }
}

See the Pen by thebabydino (@thebabydino) on CodePen.

What about having both text and some pretty extreme rounded corners? Well, that’s something we’ll discuss in another article – stay tuned!

What about backdrop-filter?

Some of you may be wondering (as I was when I started toying with various ideas in order to try to achieve this effect) whether backdrop-filter isn’t an option.

Well, yes and no!

Technically, it is possible to get the same effect, but since Firefox doesn’t yet implement it, we’re cutting out Firefox support if we choose to take this route. Not to mention this approach also forces us to use both pseudo-elements if we want the best support possible for the case when our element has some text content (which means we need the pseudos and their padding-box area background to show underneath this text).

Update: due to a regression, the backdrop-filter technique doesn’t work in Chrome anymore, so support is now limited to Safari and Edge at best.

For those who don’t yet know what backdrop-filter does: it filters out what can be seen through the (partially) transparent parts of the element we apply it on.

The way we need to go about this is the following: both pseudo-elements have a transparent border and a background positioned and sized relative to the padding-box. We restrict the background of pseudo-element on top (the :after) to the padding-box.

Now the :after doesn’t have a background in the border area anymore and we can see through to the :before pseudo-element behind it there. We set a backdrop-filter on the :after and maybe even change that border-color from transparent to slightly visible. The bottom (:before) pseudo-element’s background that’s still visible through the (partially) transparent, barely distinguishable border of the :after above gets blurred as a result of applying the backdrop-filter.

$b: 1.5em; // border-width

div {
  overflow: hidden;
  position: relative;
  padding: calc(1em + #{$b});
  /* prettifying styles */
	
  &:before, &:after {
    position: absolute;
    z-index: -1; /* put them *behind* parent */
    /* zero all offsets */
    top: 0; right: 0; bottom: 0; left: 0;
    border: solid $b transparent;
    background: $url 50%/ cover 
                  /* background-origin & -clip */
                  border-box;
    content: ''
  }
	
  &:after {
    border-color: rgba(#000, .03);
    background-clip: padding-box;
    backdrop-filter: blur(9px); /* no Firefox support */
  }
}

Remember that the live demo for this doesn’t currently work in Firefox and needs the Experimental Web Platform features flag enabled in chrome://flags in order to work in Chrome.

Eliminating one pseudo-element

This is something I wouldn’t recommend doing in the wild because it cuts out Edge support as well, but we do have a way of achieving the result we want with just one pseudo-element.

We start by setting the image background on the element (we don’t really need to explicitly set a border as long as we include its width in the padding) and then a partially transparent, barely visible background on the absolutely positioned pseudo-element that’s covering its entire parent. We also set the backdrop-filter on this pseudo-element.

$b: 1.5em; // border-width

div {
  position: relative;
  padding: calc(1em + #{$b});
  background: url(oranges.jpg) 50%/ cover;
  /* prettifying styles */
	
  &:before {
    position: absolute;
    /* zero all offsets */
    top: 0; right: 0; bottom: 0; left: 0;
    background: rgba(#000, .03);
    backdrop-filter: blur(9px); /* no Firefox support */
    content: ''
  }
}

Alright, but this blurs out the entire element behind the almost transparent pseudo-element, including its text. And it’s no bug, this is what backdrop-filter is supposed to do.

Screenshot.
The problem at hand.

In order to fix this, we need to get rid of (not make transparent, that’s completely useless in this case) the inner rectangle (whose edges are a distance $b away from the border-box edges) of the pseudo-element.

We have two ways of doing this.

The first way (live demo) is with clip-path and the zero-width tunnel technique:

$b: 1.5em; // border-width
$o: calc(100% - #{$b});

div {
  /* same styles as before */
	
  &:before {
    /* same styles as before */

    /* doesn't work in Edge */
    clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%, 
                       0 0, 
                       #{$b $b}, #{$b $o}, #{$o $o}, #{$o $b}, 
                       #{$b $b});
  }
}

The second way (live demo) is with two composited mask layers (note that, in this case, we need to explicitly set a border on our pseudo):

$b: 1.5em; // border-width

div {
  /* same styles as before */
	
  &:before {
    /* same styles as before */

    border: solid $b transparent;

    /* doesn't work in Edge */
    --fill: linear-gradient(red, red);
    -webkit-mask: var(--fill) padding-box, 
                  var(--fill);
    -webkit-mask-composite: xor;
            mask: var(--fill) padding-box exclude, 
                  var(--fill);
  }
}

Since neither of these two properties works in Edge, this means support is now limited to WebKit browsers (and we still need to enable the Experimental Web Platform features flag for backdrop-filter to work in Chrome).

Future (and better!) solution

The filter() function allows us to apply filters on individual background layers. This eliminates the need for a pseudo-element and reduces the code needed to achieve this effect to two CSS declarations!

border: solid 1.5em rgba(#000, .03);
background: $url 
              border-box /* background-origin */
              padding-box /* background-clip */, 
            filter($url, blur(9px)) 
              /* background-origin & background-clip */
              border-box

As you may have guessed, the issue here is support. Safari is the only browser to implement it at this point, but if you think the filter() is something that could help you, you can add your use cases and track implementation progress for both Chrome and Firefox.

More border filter options

I’ve only talked about blurring the border up to now, but this technique works for pretty much any CSS filter (save for drop-shadow() which wouldn’t make much sense in this context). You can play with switching between them and tweaking values in the interactive demo below:

See the Pen by thebabydino (@thebabydino) on CodePen.

And all we’ve done so far has used just one filter function, but we can also chain them and then the possibilities are endless – what cool effects can you come up with this way?

See the Pen by thebabydino (@thebabydino) on CodePen.


Blurred Borders in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/blurred-borders-in-css/feed/ 6 284773