background-clip – CSS-Tricks https://css-tricks.com Tips, Tricks, and Techniques on using Cascading Style Sheets. Fri, 18 Nov 2022 14:11:27 +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 background-clip – CSS-Tricks https://css-tricks.com 32 32 45537868 Making Static Noise From a Weird CSS Gradient Bug https://css-tricks.com/making-static-noise-from-a-weird-css-gradient-bug/ https://css-tricks.com/making-static-noise-from-a-weird-css-gradient-bug/#comments Fri, 18 Nov 2022 13:55:24 +0000 https://css-tricks.com/?p=374993 What I will be doing here is kind of an experiment to explore tricks that leverage a bug with the way CSS gradients handle sub-pixel rendering to create a static noise effect — like you might see on a TV with no signal.


Making Static Noise From a Weird CSS Gradient Bug originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
👋 The demos in this article experiment with a non-standard bug related to CSS gradients and sub-pixel rendering. Their behavior may change at any time in the future. They’re also heavy as heck. We’re serving them async where you click to load, but still want to give you a heads-up in case your laptop fan starts spinning.

Do you remember that static noise on old TVs with no signal? Or when the signal is bad and the picture is distorted? In case the concept of a TV signal predates you, here’s a GIF that shows exactly what I mean.

View image (contains auto-playing media)
Animated image showing static noise from a TV screen.

Yes, we are going to do something like this using only CSS. Here is what we’re making:

Before we start digging into the code, I want to say that there are better ways to create a static noise effect than the method I am going to show you. We can use SVG, <canvas>, the filter property, etc. In fact, Jimmy Chion wrote a good article showing how to do it with SVG.

What I will be doing here is kind of a CSS experiment to explore some tricks leveraging a bug with gradients. You can use it on your side projects for fun but using SVG is cleaner and more suitable for a real project. Plus, the effect behaves differently across browsers, so if you’re checking these out, it’s best to view them in Chrome, Edge, or Firefox.

Let’s make some noise!

To make this noise effect we are going to use… gradients! No, there is no secret ingredient or new property that makes it happen. We are going to use stuff that’s already in our CSS toolbox!

The “trick” relies on the fact that gradients are bad at anti-aliasing. You know those kind of jagged edges we get when using hard stop colors? Yes, I talk about them in most of my articles because they are a bit annoying and we always need to add or remove a few pixels to smooth things out:

As you can see, the second circle renders better than the first one because there is a tiny difference (0.5%) between the two colors in the gradient rather than using a straight-up hard color stop using whole number values like the first circle.

Here’s another look, this time using a conic-gradient where the result is more obvious:

An interesting idea struck me while I was making these demos. Instead of fixing the distortion all the time, why not trying to do the opposite? I had no idea what would happen but it was a fun surprise! I took the conic gradient values and started to decrease them to make the poor anti-aliasing results look even worse.

Do you see how bad the last one is? It’s a kind of scrambled in the middle and nothing is smooth. Let’s make it full-screen with smaller values:

I suppose you see where this is going. We get a strange distorted visual when we use very small decimal values for the hard colors stops in a gradient. Our noise is born!

We are still far from the grainy noise we want because we can still see the actual conic gradient. But we can decrease the values to very, very small ones — like 0.0001% — and suddenly there’s no more gradient but pure graininess:

Tada! We have a noise effect and all it takes is one CSS gradient. I bet if I was to show this to you before explaining it, you’d never realize you’re looking at a gradient. You have to look very carefully at center of the gradient to see it.

We can increase the randomness by making the size of the gradient very big while adjusting its position:

The gradient is applied to a fixed 3000px square and placed at the 60% 60% coordinates. We can hardly notice its center in this case. The same can be done with radial gradient as well:

And to make things even more random (and closer to a real noise effect) we can combine both gradients and use background-blend-mode to smooth things out:

Our noise effect is perfect! Even if we look closely at each example, there’s no trace of either gradient in there, but rather beautiful grainy static noise. We just turned that anti-aliasing bug into a slick feature!

Now that we have this, let’s see a few interesting examples where we might use it.

Animated no TV signal

Getting back to the demo we started with:

If you check the code, you will see that I am using a CSS animation on one of the gradients. It’s really as simple as that! All we’re doing is moving the conic gradient’s position at a lightning fast duration (.1s) and this is what we get!

I used this same technique on a one-div CSS art challenge:

Grainy image filter

Another idea is to apply the noise to an image to get an old-time-y look. Hover each image to see them without the noise.

I am using only one gradient on a pseudo-element and blending it with the image, thanks to mix-blend-mode: overlay.

We can get an even funnier effect if we use the CSS filter property

And if we add a mask to the mix, we can make even more effects!

Grainy text treatment

We can apply this same effect to text, too. Again, all we need is a couple of chained gradients on a background-image and then blend the backgrounds. The only difference is that we’re also reaching for background-clip so the effect is only applied to the bounds of each character.

Generative art

If you keep playing with the gradient values, you may get more surprising results than a simple noise effect. We can get some random shapes that look a lot like generative art!

Of course, we are far from real generative art, which requires a lot of work. But it’s still satisfying to see what can be achieved with something that is technically considered a bug!

Monster face

One last example I made for CodePen’s divtober 2022 collection:

Wrapping up

I hope you enjoyed this little CSS experiment. We didn’t exactly learn something “new” but we took a little quirk with gradients and turned it into something fun. I’ll say it again: this isn’t something I would consider using on a real project because who knows if or when anti-aliasing will be addressed at some point in time. Instead, this was a very random, and pleasant, surprise when I stumbled into it. It’s also not that easy to control and it behaves inconsistently across browsers.

This said, I am curious to see what you can do with it! You can play with the values, combine different layers, use a filter, or mix-blend-mode, or whatever, and you will for sure get something really cool. Share your creations in the comment section — there are no prizes but we can get a nice collection going!


Making Static Noise From a Weird CSS Gradient Bug originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/making-static-noise-from-a-weird-css-gradient-bug/feed/ 9 374993
Cool CSS Hover Effects That Use Background Clipping, Masks, and 3D https://css-tricks.com/css-hover-effects-background-masks-3d/ https://css-tricks.com/css-hover-effects-background-masks-3d/#comments Thu, 26 May 2022 14:13:01 +0000 https://css-tricks.com/?p=365946 We’ve walked through a series of posts now about interesting approaches to CSS hover effects. We started with a bunch of examples that use CSS background properties, then moved on to the text-shadow property where we technically didn’t use


Cool CSS Hover Effects That Use Background Clipping, Masks, and 3D originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
We’ve walked through a series of posts now about interesting approaches to CSS hover effects. We started with a bunch of examples that use CSS background properties, then moved on to the text-shadow property where we technically didn’t use any shadows. We also combined them with CSS variables and calc() to optimize the code and make it easy to manage.

In this article, we will build off those two articles to create even more complex CSS hover animations. We’re talking about background clipping, CSS masks, and even getting our feet wet with 3D perspectives. In other words, we are going to explore advanced techniques this time around and push the limits of what CSS can do with hover effects!

Cool Hover Effects series:

  1. Cool Hover Effects That Use Background Properties
  2. Cool Hover Effects That Use CSS Text Shadow
  3. Cool Hover Effects That Use Background Clipping, Masks, and 3D (you are here!)

Here’s just a taste of what we’re making:

Hover effects using background-clip

Let’s talk about background-clip. This CSS property accepts a text keyword value that allows us to apply gradients to the text of an element instead of the actual background.

So, for example, we can change the color of the text on hover as we would using the color property, but this way we animate the color change:

All I did was add background-clip: text to the element and transition the background-position. Doesn’t have to be more complicated than that!

But we can do better if we combine multiple gradients with different background clipping values.

In that example, I use two different gradients and two values with background-clip. The first background gradient is clipped to the text (thanks to the text value) to set the color on hover, while the second background gradient creates the bottom underline (thanks to the padding-box value). Everything else is straight up copied from the work we did in the first article of this series.

How about a hover effect where the bar slides from top to bottom in a way that looks like the text is scanned, then colored in:

This time I changed the size of the first gradient to create the line. Then I slide it with the other gradient that update the text color to create the illusion! You can visualize what’s happening in this pen:

We’ve only scratched the surface of what we can do with our background-clipping powers! However, this technique is likely something you’d want to avoid using in production, as Firefox is known to have a lot of reported bugs related to background-clip. Safari has support issues as well. That leaves only Chrome with solid support for this stuff, so maybe have it open as we continue.

Let’s move on to another hover effect using background-clip:

You’re probably thinking this one looks super easy compared to what we’ve just covered — and you are right, there’s nothing fancy here. All I am doing is sliding one gradient while increasing the size of another one.

But we’re here to look at advanced hover effects, right? Let’s change it up a bit so the animation is different when the mouse cursor leaves the element. Same hover effect, but a different ending to the animation:

Cool right? let’s dissect the code:

.hover {
  --c: #1095c1; /* the color */

  color: #0000;
  background: 
    linear-gradient(90deg, #fff 50%, var(--c) 0) calc(100% - var(--_p, 0%)) / 200%, 
    linear-gradient(var(--c) 0 0) 0% 100% / var(--_p, 0%) no-repeat,
    var(--_c, #0000);
  -webkit-background-clip: text, padding-box, padding-box;
          background-clip: text, padding-box, padding-box;
  transition: 0s, color .5s, background-color .5s;
}
.hover:hover {
  color: #fff;
  --_c: var(--c);
  --_p: 100%;
  transition: 0.5s, color 0s .5s, background-color 0s .5s;
}

We have three background layers — two gradients and the background-color defined using --_c variable which is initially set to transparent (#0000). On hover, we change the color to white and the --_c variable to the main color (--c).

Here’s what is happening on that transition: First, we apply a transition to everything but we delay the color and background-color by 0.5s to create the sliding effect. Right after that, we change the color and the background-color. You might notice no visual changes because the text is already white (thanks to the first gradient) and the background is already set to the main color (thanks to the second gradient).

Then, on mouse out, we apply an instant change to everything (notice the 0s delay), except for the color and background-color that have a transition. This means that we put all the gradients back to their initial states. Again, you will probably see no visual changes because the text color and background-color already changed on hover.

Lastly, we apply the fading to color and a background-color to create the mouse-out part of the animation. I know, it may be tricky to grasp but you can better visualize the trick by using different colors:

Hover the above a lot of times and you will see the properties that are animating on hover and the ones animating on mouse out. You can then understand how we reached two different animations for the same hover effect.

Let’s not forget the DRY switching technique we used in the previous articles of this series to help reduce the amount of code by using only one variable for the switch:

.hover {
  --c: 16 149 193; /* the color using the RGB format */

  color: rgb(255 255 255 / var(--_i, 0));
  background:
    /* Gradient #1 */
    linear-gradient(90deg, #fff 50%, rgb(var(--c)) 0) calc(100% - var(--_i, 0) * 100%) / 200%,
    /* Gradient #2 */
    linear-gradient(rgb(var(--c)) 0 0) 0% 100% / calc(var(--_i, 0) * 100%) no-repeat,
    /* Background Color */
    rgb(var(--c)/ var(--_i, 0));
  -webkit-background-clip: text, padding-box, padding-box;
          background-clip: text, padding-box, padding-box;
  --_t: calc(var(--_i,0)*.5s);
  transition: 
    var(--_t),
    color calc(.5s - var(--_t)) var(--_t),
    background-color calc(.5s - var(--_t)) var(--_t);
}
.hover:hover {
  --_i: 1;
}

If you’re wondering why I reached for the RGB syntax for the main color, it’s because I needed to play with the alpha transparency. I am also using the variable --_t to reduce a redundant calculation used in the transition property.

Before we move to the next part here are more examples of hover effects I did a while ago that rely on background-clip. It would be too long to detail each one but with what we have learned so far you can easily understand the code. It can be a good inspiration to try some of them alone without looking at the code.

I know, I know. These are crazy and uncommon hover effects and I realize they are too much in most situations. But this is how to practice and learn CSS. Remember, we pushing the limits of CSS hover effects. The hover effect may be a novelty, but we’re learning new techniques along the way that can most certainly be used for other things.

Hover effects using CSS mask

Guess what? The CSS mask property uses gradients the same way the background property does, so you will see that what we’re making next is pretty straightforward.

Let’s start by building a fancy underline.

I’m using background to create a zig-zag bottom border in that demo. If I wanted to apply an animation to that underline, it would be tedious to do it using background properties alone.

Enter CSS mask.

The code may look strange but the logic is still the same as we did with all the previous background animations. The mask is composed of two gradients. The first gradient is defined with an opaque color that covers the content area (thanks to the content-box value). That first gradient makes the text visible and hides the bottom zig-zag border. content-box is the mask-clip value which behaves the same as background-clip

linear-gradient(#000 0 0) content-box

The second gradient will cover the whole area (thanks to padding-box). This one has a width that’s defined using the --_p variable, and it will be placed on the left side of the element.

linear-gradient(#000 0 0) 0 / var(--_p, 0%) padding-box

Now, all we have to do is to change the value of --_p on hover to create a sliding effect for the second gradient and reveal the underline.

.hover:hover {
  --_p: 100%;
  color: var(--c);
}

The following demo uses with the mask layers as backgrounds to better see the trick taking place. Imagine that the green and red parts are the visible parts of the element while everything else is transparent. That’s what the mask will do if we use the same gradients with it.

With such a trick, we can easily create a lot of variation by simply using a different gradient configuration with the mask property:

Each example in that demo uses a slightly different gradient configuration for the mask. Notice, too, the separation in the code between the background configuration and the mask configuration. They can be managed and maintained independently.

Let’s change the background configuration by replacing the zig-zag underline with a wavy underline instead:

Another collection of hover effects! I kept all the mask configurations and changed the background to create a different shape. Now, you can understand how I was able to reach 400 hover effects without pseudo-elements — and we can still have more!

Like, why not something like this:

Here’s a challenge for you: The border in that last demo is a gradient using the mask property to reveal it. Can you figure out the logic behind the animation? It may look complex at first glance, but it’s super similar to the logic we’ve looked at for most of the other hover effects that rely on gradients. Post your explanation in the comments!

Hover effects in 3D

You may think it’s impossible to create a 3D effect with a single element (and without resorting to pseudo-elements!) but CSS has a way to make it happen.

What you’re seeing there isn’t a real 3D effect, but rather a perfect illusion of 3D in the 2D space that combines the CSS background, clip-path, and transform properties.

Breakdown of the CSS hover effect in four stages.
The trick may look like we’re interacting with a 3D element, but we’re merely using 2D tactics to draw a 3D box

The first thing we do is to define our variables:

--c: #1095c1; /* color */
--b: .1em; /* border length */
--d: 20px; /* cube depth */

Then we create a transparent border with widths that use the above variables:

--_s: calc(var(--d) + var(--b));
color: var(--c);
border: solid #0000; /* fourth value sets the color's alpha */
border-width: var(--b) var(--b) var(--_s) var(--_s);

The top and right sides of the element both need to equal the --b value while the bottom and left sides need to equal to the sum of --b and --d (which is the --_s variable).

For the second part of the trick, we need to define one gradient that covers all the border areas we previously defined. A conic-gradient will work for that:

background: conic-gradient(
  at left var(--_s) bottom var(--_s),
  #0000 90deg,var(--c) 0
 ) 
 0 100% / calc(100% - var(--b)) calc(100% - var(--b)) border-box;
Diagram of the sizing used for the hover effect.

We add another gradient for the third part of the trick. This one will use two semi-transparent white color values that overlap the first previous gradient to create different shades of the main color, giving us the illusion of shading and depth.

conic-gradient(
  at left var(--d) bottom var(--d),
  #0000 90deg,
  rgb(255 255 255 / 0.3) 0 225deg,
  rgb(255 255 255 / 0.6) 0
) border-box
Showing the angles used to create the hover effect.

The last step is to apply a CSS clip-path to cut the corners for that long shadow sorta feel:

clip-path: polygon(
  0% var(--d), 
  var(--d) 0%, 
  100% 0%, 
  100% calc(100% - var(--d)), 
  calc(100% - var(--d)) 100%, 
  0% 100%
)
Showing the coordinate points of the three-dimensional cube used in the CSS hover effect.

That’s all! We just made a 3D rectangle with nothing but two gradients and a clip-path that we can easily adjust using CSS variables. Now, all we have to do is to animate it!

Notice the coordinates from the previous figure (indicated in red). Let’s update those to create the animation:

clip-path: polygon(
  0% var(--d), /* reverses var(--d) 0% */
  var(--d) 0%, 
  100% 0%, 
  100% calc(100% - var(--d)), 
  calc(100% - var(--d)) 100%, /* reverses 100% calc(100% - var(--d)) */ 
  0% 100% /* reverses var(--d) calc(100% - var(--d)) */
)

The trick is to hide the bottom and left parts of the element so all that’s left is a rectangular element with no depth whatsoever.

This pen isolates the clip-path portion of the animation to see what it’s doing:

The final touch is to move the element in the opposite direction using translate — and the illusion is perfect! Here’s the effect using different custom property values for varying depths:

The second hover effect follows the same structure. All I did is to update a few values to create a top left movement instead of a top right one.

Combining effects!

The awesome thing about everything we’ve covered is that they all complement each other. Here is an example where I am adding the text-shadow effect from the second article in the series to the background animation technique from the first article while using the 3D trick we just covered:

The actual code might be confusing at first, but go ahead and dissect it a little further — you’ll notice that it’s merely a combination of those three different effects, pretty much smushed together.

Let me finish this article with a last hover effect where I am combining background, clip-path, and a dash of perspective to simulate another 3D effect:

I applied the same effect to images and the result was quite good for simulating 3D with a single element:

Want a closer look at how that last demo works? I wrote something up on it.

Wrapping up

Oof, we are done! I know, it’s a lot of tricky CSS but (1) we’re on the right website for that kind of thing, and (2) the goal is to push our understanding of different CSS properties to new levels by allowing them to interact with one another.

You may be asking what the next step is from here now that we’re closing out this little series of advanced CSS hover effects. I’d say the next step is to take all that we learned and apply them to other elements, like buttons, menu items, links, etc. We kept things rather simple as far as limiting our tricks to a heading element for that exact reason; the actual element doesn’t matter. Take the concepts and run with them to create, experiment with, and learn new things!


Cool CSS Hover Effects That Use Background Clipping, Masks, and 3D originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/css-hover-effects-background-masks-3d/feed/ 5 365946
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
CSS-ing Candy Ghost Buttons https://css-tricks.com/css-ing-candy-ghost-buttons/ https://css-tricks.com/css-ing-candy-ghost-buttons/#comments Mon, 01 Nov 2021 01:04:53 +0000 https://css-tricks.com/?p=354804 Recently, while looking for some ideas on what to code as I have zero artistic sense so the only thing I can do is find pretty things that other people have come up with and remake them with clean and …


CSS-ing Candy Ghost Buttons originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Recently, while looking for some ideas on what to code as I have zero artistic sense so the only thing I can do is find pretty things that other people have come up with and remake them with clean and compact code… I came across these candy ghost buttons!

They seemed like the perfect choice for a cool little thing I could quickly code. Less than fifteen minutes later, this was my Chromium result:

Chrome screenshot. Shows a four row, five column grid of candy ghost buttons with text and an icon following it. These buttons have an elongated pill-like shape, a transparent background and a continuous sweet pastel gradient for the border and the text and icon inside.
The pure CSS candy ghost buttons.

I thought the technique was worth sharing, so in this article, we’ll be going through how I first did it and what other options we have.

The starting point

A button is created with… are you ready for this? A button element! This button element has a data-ico attribute in which we drop an emoji. It also has a stop list custom property, --slist, set in the style attribute.

<button data-ico="👻" style="--slist: #ffda5f, #f9376b">boo!</button>

After writing the article, I learned that Safari has a host of problems with clipping to text, namely, it doesn’t work on button elements, or on elements with display: flex (and perhaps grid too?), not to mention the text of an element’s children. Sadly, this means all of the techniques presented here fail in Safari. The only workaround is to apply all the button styles from here on a span element nested inside the button, covering its parent’s border-box. And, in case this helps anyone else who, like me, is on Linux without physical access to an Apple device (unless you count the iPhone 5 someone on the fourth floor — who you don’t want to bother with stuff like this more than twice a month anyway — bought recently), I’ve also learned to use Epiphany in the future. Thanks to Brian for the suggestion!

For the CSS part, we add the icon in an ::after pseudo-element and use a grid layout on the button in order to have nice alignment for both the text and the icon. On the button, we also set a border, a padding, a border-radius, use the stop list, --slist, for a diagonal gradient and prettify the font.

button {
  display: grid;
  grid-auto-flow: column;
  grid-gap: .5em;
  border: solid .25em transparent;
  padding: 1em 1.5em;
  border-radius: 9em;
  background: 
    linear-gradient(to right bottom, var(--slist)) 
      border-box;
  font: 700 1.5em/ 1.25 ubuntu, sans-serif;
  text-transform: uppercase;
  
  &::after { content: attr(data-ico) }
}

There’s one thing to clarify about the code above. On the highlighted line, we set both the background-origin and the background-clip to border-box. background-origin both puts the 0 0 point for background-position in the top-left corner of the box it’s set to and gives us the box whose dimensions the background-size is relative to.

That is, if background-origin is set to padding-box, the 0 0 point for background-position is in the top left-corner of the padding-box. If background-origin is set to border-box, the 0 0 point for background-position is in the top-left corner of the border-box. If background-origin is set to padding-box, a background-size of 50% 25% means 50% of the padding-box width and 25% of the padding-box height. If background-origin is set to border-box, the same background-size of 50% 25% means 50% of the border-box width and 25% of the border-box height.

The default value for background-origin is padding-box, meaning that a default-sized 100% 100% gradient will cover the padding-box and then repeat itself underneath the border (where we cannot see it if the border is fully opaque). However, in our case, the border is fully transparent and we want our gradient to stretch across the entire border-box. This means we need to change the background-origin value to border-box.

Screenshot collage. Chrome on the left, Firefox on the right, showing differences between ghost emojis. The button has a pastel gradient background going along the main diagonal, the text 'Boo!' in black and a ghost emoji, which is going to look different depending on the OS and browser.
The result after applying the base styles (live demo).

The simple, but sadly non-standard Chromium solution

This involves using three mask layers and compositing them. If you need a refresher on mask compositing, you can check out this crash course.

Note that in the case of CSS mask layers, only the alpha channel matters, as every pixel of the masked element gets the alpha of the corresponding mask pixel, while the RGB channels don’t influence the result in any way, so they may be any valid value. Below, you can see the effect of a purple to transparent gradient overlay versus the effect of using the exact same gradient as a mask.

Screenshot. Shows two Halloween-themed cat pictures (the cat is protectively climbed on top of a Halloween pumpkin) side by side. The first one has a purple to transparent linear gradient overlay on top. The second one uses the exact same linear gradient as a mask. By default, CSS masks are alpha masks, meaning that every pixel of the masked element gets the alpha of the corresponding mask pixel.
Gradient overlay vs. the same gradient mask (live demo).

We’re going to start with the bottom two layers. The first one is a fully opaque layer, fully covering the entire border-box, meaning that it has an alpha of 1 absolutely everywhere. The other one is also fully opaque, but restricted (by using mask-clip) to the padding-box, which means that, while this layer has an alpha of 1 all across the padding-box, it’s fully transparent in the border area, having an alpha of 0 there.

If you have a hard time picturing this, a good trick is to think of an element’s layout boxes as nested rectangles, the way they’re illustrated below.

Illustration showing the layout boxes. The outermost box is the border-box. Inside it, a border-width away from the border limit, we have the padding-box. And finally, inside the padding-box, a padding away from the padding limit, we have the content-box.
The layout boxes (live demo).

In our case, the bottom layer is fully opaque (the alpha value is 1) across the entire orange box (the border-box). The second layer, that we place on top of the first one, is fully opaque (the alpha value is 1) all across the red box (the padding-box) and fully transparent (with an alpha of 0) in the area between the padding limit and the border limit.

One really cool thing about the limits of these boxes is that corner rounding is determined by the border-radius (and, in the case of the padding-box, by the border-width as well). This is illustrated by the interactive demo below, where we can see how the corner rounding of the border-box is given by the border-radius value, while the corner rounding of the padding-box is computed as the border-radius minus the border-width (limited at 0 in case the difference is a negative value).

Now let’s get back to our mask layers, one being fully opaque all across the entire border-box, while the one on top of it is fully opaque across the padding-box area and fully transparent in the border area (between the padding limit and the border limit). These two layers get composited using the exclude operation (called xor in the non-standard WebKit version).

Illustration. Shows the bottom two background layers in 3D. The first one from the bottom has an alpha of 1 all across the entire border-box. The second one, layered on top of it, has an alpha of 1 across the padding box, within the padding limit; it also has an alpha of 0 in the border area, outside the padding limit, but inside the border limit.
The two base layers (live demo).

The name of this operation is pretty suggestive in the situation where the alphas of the two layers are either 0 or 1, as they are in our case — the alpha of the first layer is 1 everywhere, while the alpha of the second layer (that we place on top of the first) is 1 inside the padding-box and 0 in the border area between the padding limit and the border limit.

In this situation, it’s pretty intuitive that the rules of boolean logic apply — XOR-ing two identical values gives us 0, while XOR-ing two different ones gives us 1.

All across the padding-box, both the first layer and the second one have an alpha of 1, so compositing them using this operation gives us an alpha of 0 for the resulting layer in this area. However, in the border area (outside the padding limit, but inside the border limit), the first layer has an alpha of 1, while the second one has an alpha of 0, so we get an alpha of 1 for the resulting layer in this area.

This is illustrated by the interactive demo below, where you can switch between viewing the two mask layers separated in 3D and viewing them stacked and composited using this operation.

Putting things into code, we have:

button {
  /* same base styles */
  --full: linear-gradient(red 0 0);
  -webkit-mask: var(--full) padding-box, var(--full);
  -webkit-mask-composite: xor;
  mask: var(--full) padding-box exclude, var(--full);
}

Before we move further, let’s discuss a few fine-tuning details about the CSS above.

First off, since the fully opaque layers may be anything (the alpha channel is fixed, always 1 and the RGB channels don’t mater), I usually make them red — only three characters! In the same vein, using a conic gradient instead of a linear one would also save us one character, but I rarely ever do that since we still have mobile browsers that support masking, but not conic gradients. Using a linear one ensures we have support all across the board. Well, save for IE and pre-Chromium Edge but, then again, not much cool shiny stuff works in those anyway.

Second, we’re using gradients for both layers. We’re not using a plain background-color for the bottom one because we cannot set a separate background-clip for the background-color itself. If we were to have the background-image layer clipped to the padding-box, then this background-clip value would also apply to the background-color underneath — it would be clipped to the padding-box too and we’d have no way to make it cover the entire border-box.

Third, we’re not explicitly setting a mask-clip value for the bottom layer since the default for this property is precisely the value we want in this case, border-box.

Fourth, we can include the standard mask-composite (supported by Firefox) in the mask shorthand, but not the non-standard one (supported by WebKit browsers).

And finally, we always set the standard version last so it overrides any non-standard version that may also be supported.

The result of our code so far (still cross-browser at this point) looks like below. We’ve also added a background-image on the root so that it’s obvious we have real transparency across the padding-box area.

Screenshot. The pastel gradient button is just a shadow of its former self. Well, just a border, that's all we can see of it. The entire area inside the padding limit has been masked out and we can now see through to the image background behind the button.
The result after masking out the entire padding-box (live demo).

This is not what we want. While we have a nice gradient border (and by the way, this is my preferred method of getting a gradient border since we have real transparency all across the padding-box and not just a cover), we are now missing the text.

So the next step is to add back the text using yet another mask layer on top of the previous ones, this time one that’s restricted to text (while also making the actual text fully transparent so that we can see the gradient background through it) and XOR this third mask layer with the result of XOR-ing the first two (result that can be seen in the screenshot above).

The interactive demo below allows viewing the three mask layers both separated in 3D as well as stacked and composited.

Note that the text value for mask-clip is non-standard, so, sadly, this only works in Chrome. In Firefox, we just don’t get any masking on the button anymore and having made the text transparent, we don’t even get graceful degradation.

button {
  /* same base styles */
  -webkit-text-fill-color: transparent;
  --full: linear-gradient(red 0 0);
  -webkit-mask: var(--full) text, var(--full) padding-box, var(--full);
  -webkit-mask-composite: xor;
  /* sadly, still same result as before :( */
  mask: var(--full) padding-box exclude, var(--full);
}

If we don’t want to make our button unusable this way, we should put the code that applies the mask and makes the text transparent in a @supports block.

button {
  /* same base styles */

  @supports (-webkit-mask-clip: text) {
    -webkit-text-fill-color: transparent;
    --full: linear-gradient(red 0 0);
    -webkit-mask: var(--full) text, var(--full) padding-box, var(--full);
    -webkit-mask-composite: xor;
  }
}
Screenshot collage. Chrome (left) vs. Firefox (right). In Chrome, we have a real pill-shaped pastel gradient ghost button. It has a transparent background that lets us see through to the image background behind our button and a continuous sweet pastel gradient for the border and the text and icon inside. In Firefox, we have the same pill-shaped, pastel background, black text and normal emoji button we had after setting the base styles. The ghost emoji is going to look different depending on the OS and browser - here it can be seen it has different looks in Chrome and Firefox.
The final result using the masking-only method (live demo).

I really like this method, it’s the simplest we have at this point and I’d really wish text was a standard value for mask-clip and all browsers supported it properly.

However, we also have a few other methods of achieving the candy ghost button effect, and although they’re either more convoluted or more limited than the non-standard Chromium-only one we’ve just discussed, they’re also better supported. So let’s take a look at those.

The extra pseudo-element solution

This involves setting the same initial styles as before, but, instead of using a mask, we clip the background to the text area.

button {
  /* same base styles */
  background: 
    linear-gradient(to right bottom, var(--slist)) 
    border-box;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent
}

Just like before, we need to also make the actual text transparent, so we can see through it to the pastel gradient background behind it that is now clipped to its shape.

Screenshot collage. Chrome (left) vs. Firefox (right), highlighting the differences in emoji shapes when they're part of knockout text. This is entirely normal and fine, as emojis look different depending on OS and browser.
Knockout button text (live demo).

Alright, we have the gradient text, but now we’re missing the gradient border. So we’re going to add it using an absolutely positioned ::before pseudo-element that covers the entire border-box area of the button and inherits the border, border-radius and background from its parent (save for the background-clip, which gets reset to border-box).

$b: .25em;

button {
  /* same as before */
  position: relative;
  border: solid $b transparent;
  
  &::before { 
    position: absolute;
    z-index: -1;
    inset: -$b;
    border: inherit;
    border-radius: inherit;
    background: inherit;
    background-clip: border-box;
    content: '';
  }
}

inset: -$b is a shorthand for:

top: -$b;
right: -$b;
bottom: -$b;
left: -$b

Note that we’re using the border-width value ($b) with a minus sign here. The 0 value would make the margin-box of the pseudo (identical to the border-box in this case since we have no margin on the ::before) only cover the padding-box of its button parent and we want it to cover the entire border-box. Also, the positive direction is inwards, but we need to go outwards by a border-width to get from the padding limit to the border limit, hence the minus sign — we’re going in the negative direction.

We’ve also set a negative z-index on this absolutely positioned element since we don’t want it to be on top of the button text and prevent us from selecting it. At this point, text selection is the only way we can distinguish the text from the background, but we’ll soon fix that!

Screenshot. Shows how text selection is the only way of still distinguishing the transparent text and gradient background clipped to text area button from its gradient background ::before pseudo that covers it fully.
The result after adding the gradient pseudo (live demo).

Note that since pseudo-element content isn’t selectable, the selection only includes the button’s actual text content and not the emoji in the ::after pseudo-element as well.

The next step is to add a two layer mask with an exclude compositing operation between them in order to leave just the border area of this pseudo-element visible.

button {
  /* same as before */
    
  &::before { 
    /* same as before */
    --full: linear-gradient(red 0 0);
    -webkit-mask: var(--full) padding-box, var(--full);
    -webkit-mask-composite: xor;
    mask: var(--full) padding-box exclude, var(--full);
  }
}

This is pretty much what we did for the actual button in one of the intermediate stages of the previous method.

Screenshot collage. Chrome (left) vs. Firefox (right). Both display a pill-shaped pastel gradient ghost button. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
The final result using the extra pseudo method (live demo).

I find this to be the best approach in most cases when we want something cross-browser and that doesn’t include IE or pre-Chromium Edge, none of which ever supported masking.

The border-image solution

At this point, some of you may be screaming at the screen that there’s no need to use the ::before pseudo-element when we could use a gradient border-image to create this sort of a ghost button — it’s a tactic that has worked for over three quarters of a decade!

However, there’s a very big problem with using border-image for pill-shaped buttons: this property doesn’t play nice with border-radius, as it can be seen in the interactive demo below. As soon as we set a border-image on an element with border-radius, we lose the corner rounding of the border, even through, funny enough, the background will still respect this rounding.

Still, this may be a simple solution in the case where don’t need corner rounding or the desired corner rounding is at most the size of the border-width.

In the no corner rounding case, save for dropping the now pointless border-radius, we don’t need to change the initial styles much:

button {
  /* same base styles */
  --img: linear-gradient(to right bottom, var(--slist));
  border: solid .25em;
  border-image: var(--img) 1;
  background: var(--img) border-box;
  -webkit-background-clip: text;
  background-clip: text;
  -webkit-text-fill-color: transparent;
}

The result can be seen below, cross-browser (should be supported supported even in pre-Chromium Edge).

Screenshot collage. Chrome (left) vs. Firefox (right). Both display a pastel gradient ghost button with no rounded corners. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
The no corner rounding result using the border-image method (live demo).

The trick with the desired corner rounding being smaller than the border-width relies on the way border-radius works. When we set this property, the radius we set represents the rounding for the corners of the border-box. The rounding for the corners of the padding-box (which is the inner rounding of the border) is the border-radius minus the border-width if this difference is positive and 0 (no rounding) otherwise. This means we have no inner rounding for the border if the border-radius is smaller than or equal to the border-width.

In this situation, we can use the inset() function as a clip-path value since it also offers the possibility of rounding the corners of the clipping rectangle. If you need a refresher on the basics of this function, you can check out the illustration below:

Illustration of how inset(d round r) works. Shows the clipping rectangle inside the element's border-box, its edges all a distance d away from the border limit. The corners of this clipping rectangle all have a rounding r along both axes.
How the inset() function works.

inset() cuts out everything outside a clipping rectangle defined by the distances to the edges of the element’s border-box, specified the same way we’d specify margin, border or padding (with one, two, three or four values) and the corner rounding for this rectangle, specified the same way we’d specify border-radius (any valid border-radius value is also valid here).

In our case, the distances to the edges of the border-box are all 0 (we don’t want to chop anything off any of the edges of the button), but we have a rounding that has to be at most at big as the border-width so that not having any inner border rounding makes sense.

$b: .25em;

button {
  /* same as before */
  border: solid $b transparent;
  clip-path: inset(0 round $b)
}

Note that the clip-path is also going to cut out any outer shadows we may add on the button element, whether they’re added via box-shadow or filter: drop-shadow().

Screenshot collage. Chrome (left) vs. Firefox (right). Both display a pastel gradient ghost button with small rounded corners, the rounding radius being the same size as the border-width. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
The small corner rounding result using the border-image method (live demo).

While this technique cannot achieve the pill shape look, it does have the advantage of having great support nowadays and it may be all we need in certain situations.

The three solutions discussed so far can be seen compiled in the demo below, which also comes with a YouTube link where you can see me code each of them from scratch if you prefer to learn by watching things being built on video rather than reading about them.

All these methods create real transparency in the padding-box outside the text, so they work for any background we may have behind the button. However, we also have a couple of other methods that may be worth mentioning, even though they come with restrictions in this department.

The cover solution

Just like the border-image approach, this is a pretty limited tactic. It doesn’t work unless we have a solid or a fixed background behind the button.

It involves layering backgrounds with different background-clip values, just like the cover technique for gradient borders. The only difference is that here we add one more gradient layer on top of the one emulating the background behind our button element and we clip this top layer to text.

$c: #393939;

html { background: $c; } 

button {
  /* same as before */
  --grad: linear-gradient(to right bottom, var(--slist));
  border: solid .25em transparent;
  border-radius: 9em;
  background: var(--grad) border-box, 
              linear-gradient($c 0 0) /* emulate bg behind button */, 
              var(--grad) border-box;
  -webkit-background-clip: text, padding-box, border-box;
  -webkit-text-fill-color: transparent;
}

Sadly, this approach fails in Firefox due to an old bug — just not applying any background-clip while also making the text transparent produces a pill-shaped button with no visible text.

Screenshot collage. Chrome (left) vs. Firefox (right). Chrome displays a pill-shaped pastel gradient ghost button. Firefox sadly only displays a pill-shaped button with no visible text.
The all background-clip cover solution (live demo).

We could still make it cross-browser by using the cover method for the gradient border on a ::before pseudo and background-clip: text on the actual button, which is basically just a more limited version of the second solution we discussed — we still need to use a pseudo, but, since we use a cover, not a mask, it only works if we have a solid or fixed background behind the button.

$b: .25em;
$c: #393939;

html { background: $c; } 

button {
  /* same base styles */
  --grad: linear-gradient(to right bottom, var(--slist));
  border: solid $b transparent;
  background: var(--grad) border-box;
  -webkit-background-clip: text;
          background-clip: text;
  -webkit-text-fill-color: transparent;
  
  &::before {
    position: absolute;
    z-index: -1;
    inset: -$b;
    border: inherit;
    border-radius: inherit;
    background: linear-gradient($c 0 0) padding-box, 
                var(--grad) border-box;
    content: '';
  }
}

On the bright side, this more limited version should also work in pre-Chromium Edge.

Screenshot collage. Chrome (left) vs. Firefox (right). Both display a pill-shaped pastel gradient ghost button that has a solid background behind. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
The cover solution on a pseudo for a solid background behind the button (live demo).

Below, there’s also the fixed background version.

$f: url(balls.jpg) 50%/ cover fixed;

html { background: $f; } 

button {
  /* same as before */
  
  &::before {
    /* same as before */
    background: $f padding-box, 
                var(--grad) border-box
  }
}
Screenshot collage. Chrome (left) vs. Firefox (right). Both display a pill-shaped pastel gradient ghost button that has a fixed image background behind. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
The cover solution on a pseudo for a fixed background behind the button (live demo).

Overall, I don’t think this is the best tactic unless we both fit into the background limitation and we need to reproduce the effect in browsers that don’t support masking, but support clipping the background to the text, such as pre-Chromium Edge.

The blending solution

This approach is another limited one as it won’t work unless, for each and every gradient pixel that’s visible, its channels have values that are either all bigger or all smaller than than the corresponding pixel of the background underneath the button. However, this is not the worst limitation to have as it should probably lead to our page having better contrast.

Here, we start by making the parts where we want to have the gradient (i.e. the text, icon and border) either white or black, depending on whether we have a dark theme with a light gradient or a light theme with a dark gradient, respectively. The rest of the button (the area around the text and icon, but inside the border) is the inverse of the previously chosen color (white if we set the color value to black and black otherwise).

In our case, we have a pretty light gradient button on a dark background, so we start with white for the text, icon and border, and black for the background. The hex channel values of our two gradient stops are ff (R), da (G), 5f (B) and f9 (R), 37 (G), 6b (B), so we’d be safe with any background pixels whose channel values are at most as big as min(ff, f9) = f9 for red, min(da, 37) = 37 for green and min(5f, 6b) = 5f for blue.

This means having a background-color behind our button with channel values that are smaller or equal to f9, 37 and 5f, either on its own as a solid background, or underneath a background-image layer we blend with using the multiply blend mode (which always produces a result that’s at least as dark as the darker of the two layers). We’re setting this background on a pseudo-element since blending with the actual body or the html doesn’t work in Chrome.

$b: .25em;

body::before {
  position: fixed;
  inset: 0;
  background: url(fog.jpg) 50%/ cover #f9375f;
  background-blend-mode: multiply;
  content: '';
}

button {
  /* same base styles */
  position: relative; /* so it shows on top of body::before */
  border: solid $b;
  background: #000;
  color: #fff;
  
  &::after {
    filter: brightness(0) invert(1);
    content: attr(data-ico);
  }
}

Note that making the icon fully white means making it first black with brightness(0) and then inverting this black with invert(1).

Screenshot collage. Chrome (left) vs. Firefox (right). Both show a pill-shaped black and white (white border, white text, white emoji and black everything in between) button on top of a dark image background. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
The black and white button (live demo).

We then add a gradient ::before pseudo-element, just like we did for the first cross-browser method.

button {
  /* same styles as before */
  position: relative;
  
  &::before {
    position: absolute;
    z-index: 2;
    inset: -$b;
    border-radius: inherit;
    background: linear-gradient(to right bottom, var(--slist);
    pointer-events: none;
    content: '';
  }
}

The only difference is that here, instead of giving it a negative z-index, we give it a positive z-index. That way it’s not just over the actual button, but also over the ::after pseudo and we set pointer-events to none in order to allow the mouse to interact with the actual button content underneath.

Screenshot. Shows a pill-shaped gradient button with no visible text on top of a dark image background.
The result after adding a gradient pseudo on top of the black and white button (live demo).

Now the next step is to keep the black parts of our button, but replace the white parts (i.e., the text, icon and border) with the gradient. We can do this with a darken blend mode, where the two layers are the black and white button with the ::after icon and the gradient pseudo on top of it.

For each of the RGB channels, this blend mode takes the values of the two layers and uses the darker (smaller) one for the result. Since everything is darker than white, the resulting layer uses the gradient pixel values in that area. Since black is darker than everything, the resulting layer is black everywhere the button is black.

button {
  /* same styles as before */
  
  &::before {
    /* same styles as before */
    mix-blend-mode: darken;
  }
}
Screenshot collage.  Chrome (left) vs. Firefox (right). Both show a pill-shaped black and pastel gradient (pastel gradient border, text, emoji and black everything in between) button on top of a dark image background. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
The “almost there” result (live demo).

Alright, but we’d only be done at this point if the background behind the button was pure black. Otherwise, in the case of a background whose every pixel is darker than the corresponding pixel of the gradient on our button, we can apply a second blend mode, this time lighten on the actual button (previously, we had darken on the ::before pseudo).

For each of the RGB channels, this blend mode takes the values of the two layers and uses the lighter (bigger) one for the result. Since anything is lighter than black, the resulting layer uses the background behind the button everywhere the button is black. And since a requirement is that every gradient pixel of the button is lighter than the corresponding pixel of the background behind it, the resulting layer uses the gradient pixel values in that area.

button {
  /* same styles as before */
  mix-blend-mode: lighten;
}
Screenshot collage. Chrome (left) vs. Firefox (right). Both show a pill-shaped pastel gradient ghost with a 'BOO!' text and a ghost emoji button on top of a dark image background. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
The light ghost button on top of a dark background (live demo).

For a dark gradient button on a light background, we need to switch up the blend modes. That is, use lighten on the ::before pseudo and darken on the button itself. And first of all, we need to ensure the background behind the button is light enough.

Let’s say our gradient is between #602749 and #b14623. The channel values of our gradient stops are 60 (R), 27 (G), 49 (B) and b1 (R), 46 (G), 23 (R), so the background behind the button needs to have channel values that are at least max(60, b1) = b1 for red, max(27, 46) = 46 for green and max(49, 23) = 49 for blue.

This means having a background-color on our button with channel values that are bigger or equal to b1, 46 and 49, either on its own as a solid background, or underneath a background-image layer, uses a screen blend mode (which always produces a result that’s at least as light as the lighter of the two layers).

We also need to make the button border, text and icon black, while setting its background to white:

$b: .25em;

section {
  background: url(fog.jpg) 50%/ cover #b14649;
  background-blend-mode: screen;
}

button {
  /* same as before */
  border: solid $b;
  background: #fff;
  color: #000;
  mix-blend-mode: darken;

  &::before {
    /* same as before */
    mix-blend-mode: lighten
  }
  
  &::after {
    filter: brightness(0);
    content: attr(data-ico);
  }
}

The icon in the ::after pseudo-element is made black by setting filter: brightness(0) on it.

Screenshot collage. Chrome (left) vs. Firefox (right). Both show a pill-shaped dark gradient ghost with a 'BOO!' text and a ghost emoji button on top of a light image background. The only difference is in the shape of the emoji. This is entirely normal and fine, as emojis look different depending on OS and browser.
The dark ghost button on top of a light background (live demo).

We also have the option of blending all the button layers as a part of its background, both for the light and dark theme, but, as mentioned before, Firefox just ignores any background-clip declaration where text is a part of a list of values and not the single value.

Well, that’s it! I hope you’re having (or had) a scary Halloween. Mine was definitely made horrific by all the bugs I got to discover… or rediscover, along with the reality that they haven’t been fixed in the meanwhile.


CSS-ing Candy Ghost Buttons originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/css-ing-candy-ghost-buttons/feed/ 5 354804
Nested Gradients with background-clip https://css-tricks.com/nested-gradients-with-background-clip/ https://css-tricks.com/nested-gradients-with-background-clip/#comments Wed, 28 Aug 2019 21:31:09 +0000 https://css-tricks.com/?p=294589 I can’t say I use background-clip all that often. I’d wager it’s hardly ever used in day-to-day CSS work. But I was reminded of it in a post by Stefan Judis, which coincidentally was itself a learning-response post to …


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

]]>
I can’t say I use background-clip all that often. I’d wager it’s hardly ever used in day-to-day CSS work. But I was reminded of it in a post by Stefan Judis, which coincidentally was itself a learning-response post to a post over here by Ana Tudor.

Here’s a quick explanation.

You’ve probably seen this thing a million times:

The box model visualizer in DevTools.

That’s showing you the size and position of an element, as well as how that size is made up: content size, padding, margin, and border.

Those things aren’t just theoretical to help with understanding and debugging. Elements actually have a content-box, padding-box, and border-box. Perhaps we encounter that most often when we literally set the box-sizing property. (It’s tremendously useful to universally set it to border-box).

Those values are the same values as background-clip uses! Meaning that you can set a background to only cover those specific areas. And because multiple backgrounds is a thing, that means we can have multiple backgrounds with different clipping on each.

Like this:

See the Pen
Multiple background-clip
by Chris Coyier (@chriscoyier)
on CodePen.

But that’s boring and there are many ways to pull off that effect, like using borders, outline, and box-shadow or any combination of them.

What is more interesting is the fact that those backgrounds could be gradients, and that’s a lot harder to pull off any other way!

See the Pen
Nested Gradients
by Chris Coyier (@chriscoyier)
on CodePen.


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

]]>
https://css-tricks.com/nested-gradients-with-background-clip/feed/ 5 294589
Multiple Background Clip https://css-tricks.com/multiple-background-clip/ Wed, 30 Jan 2019 22:39:16 +0000 http://css-tricks.com/?p=282018 You know how you can have multiple backgrounds?

body {
  background-image: 
    url(image-one.jpg),
    url(image-two.jpg);
}

That’s just background-image. You can set their position too, as you might expect. We’ll shorthand it:

body {
  background: 
    url(image-one.jpg) no-repeat top right,
    url(image-two.jpg) 


Multiple Background Clip originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
You know how you can have multiple backgrounds?

body {
  background-image: 
    url(image-one.jpg),
    url(image-two.jpg);
}

That’s just background-image. You can set their position too, as you might expect. We’ll shorthand it:

body {
  background: 
    url(image-one.jpg) no-repeat top right,
    url(image-two.jpg) no-repeat bottom left;
}

I snuck background-repeat in there just for fun. Another one you might not think of setting for multiple different backgrounds, though, is background-clip. In this linked article, Stefan Judis notes that this unlocks some pretty legit CSS-Trickery!

To Shared LinkPermalink on CSS-Tricks


Multiple Background Clip originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
282018
The `background-clip` Property and its Use Cases https://css-tricks.com/the-backgound-clip-property-and-use-cases/ https://css-tricks.com/the-backgound-clip-property-and-use-cases/#comments Fri, 05 Feb 2016 10:00:42 +0000 http://css-tricks.com/?p=237034 background-clip is one of those properties I’ve known about for years, but rarely used. Maybe just a couple of times as part of a solution to a Stack Overflow question. Until last year, when I started creating my huge collection


The `background-clip` Property and its Use Cases originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
background-clip is one of those properties I’ve known about for years, but rarely used. Maybe just a couple of times as part of a solution to a Stack Overflow question. Until last year, when I started creating my huge collection of sliders. Some of the designs I chose to reproduce were a bit more complex and I only had one element available per slider, which happened to be an input element, meaning that I couldn’t even use pseudo-elements on it. Even though that does work in certain browsers, the fact that it works is actually a bug and I didn’t want to rely on that. All this meant I ended up using backgrounds, borders, and shadows a lot. I also learned a lot from doing that and this article shares some of those lessons.

Before anything else, let’s see what background-clip is and what it does.

In the following image, we have an element’s box model.

the box model

If the padding is 0, then the padding-box is exactly the same size as the content-box, and the content limit coincides with the padding limit.

with padding: 0

If the border-width is 0, the border-box is the same size as the padding-box, and the border limit coincides with the padding limit.

with border-width: 0

If both the padding and the border-width are 0, then all the three boxes (the content-box, the padding-box, and the border-box) have the same size, and the content limit, the padding limit, and the border limit all coincide.

with padding: 0 and border-width: 0

By default, backgrounds cover the entire border-box (they are applied underneath the border as well), but their background-position (and also %-based background-size) is relative to the padding-box.

In order to better understand this, let’s consider an example. We take a box with random dimensions, give it a simple gradient background of background-size: 50% 50% and a hashed border (using border-image) so we can still see through the hashes what’s underneath the border:

See the Pen default background-origin and background-clip (basic version) by CSS-Tricks (@css-tricks) on CodePen.

In this demo, we can see that the gradient background covers the entire border-box (it’s visible underneath the hashed border). We haven’t specified a background-position, so it takes the default value — 0 0. We can see this is relative to the padding-box because it starts from the top left corner (the 0 0) of this box. We can also see that the background-size set in % is relative to the padding-box.

by default, backgrounds cover the entire border-box, but start from the top left corner of the padding-box

When setting the background-size for a gradient (but not for actual images), we usually need two values for consistent results across browsers. If we use just one value, Firefox takes the second one to be 100% (per spec), while every other browser incorrectly takes the second value to be equal to the first. A missing background-size value is taken to be auto and since gradients have no intrinsic dimensions or intrinsic proportions, the auto value cannot be resolved from those, so it should get treated as 100%. So unless we want both dimensions of our background-size to be 100%, we should use two values.

Using just one value for setting the background-size doesn’t produce consistent results across browsers (live test); left: Firefox (per spec, takes the second value to be 100%); right: Chrome/ Opera, Safari, IE/ Edge (incorrectly take the second value to be equal to the first)

We can make the background cover just the padding-box or just the content-box with the help of background-clip. Clipping means cutting out and not displaying what falls outside the clipping region, where the clipping region is the area inside the dotted line in the illustration below.

illustration of what clipping is

In the default case of background-clip: border-box, the clipping region is the border-box, so we have the background underneath the border as well.

background-clip: border-box

If we set background-clip: padding-box, the clipping region is the padding-box, meaning that the background is only displayed within the padding-box limit (it doesn’t go underneath the border).

background-clip: padding-box

And finally, if we have background-clip: content-box, the clipping region is the content-box, so the background is only displayed within the content-box limit.

background-clip: content-box

These three situations are illustrated by the following live demo:

See the Pen backgrounds – helper demo #2 by CSS-Tricks (@css-tricks) on CodePen.

We also have another property called background-origin that specifies which of the three boxes the background-position (and background-size, if expressed in %) is relative to.

Let’s consider that we have an element like before, with a hashed border and, this time, a visible padding. We layer an actual image and a gradient on the background. Both have background-size: 50% 50% and are not repeating. In addition to this, the image has background-position: 100% 100% (we leave the default 0 0 for the gradient):

background: linear-gradient(to right bottom, 
      #e18728, #be4c39, #9351a6, #4472b9), 
  url(tiger_lily.jpg) 100% 100%;
background-repeat: no-repeat;
background-size: 50% 50%;

The following demo illustrates what happens for each of the three possible values for background-originborder-box, padding-box, and content-box:

See the Pen backgrounds – helper demo #3 by CSS-Tricks (@css-tricks) on CodePen.

The 100% 100% specified by the background-position of the actual image is the 100% 100% of the box specified by background-origin. At the same time, the 50% 50% specified by the background-size means half the width and half the height of the box specified by background-origin.

In the background shorthand, background-origin and background-clip can be specified in this order at the end of the layer. Since they both take a box value, if just one box value is specified, then both are set to it. If two box values are specified, background-origin is set to the first and background-clip is set to the second. If no box values are specified, they just take the default values (padding-box for background-origin and border-box for background-clip).

All right! Now let’s see how we can use this to our advantage!

Transparent gap between border and background

Some may remember we can get semitransparent borders with background-clip. But, we can also introduce a space between the border and the area with a background without introducing an extra element. The simplest way to do this is to have a padding in addition to a border, and also set background-clip to content-box. By doing it in the shorthand with just one box value, we’re also setting background-origin to content-box, but that’s fine in this case, it doesn’t have any unwanted effect.

border: solid .5em #be4c39;
padding: .5em;
background: #e18728 content-box;

Clipping the background to content-box means it doesn’t extend beyond the content limit. Beyond that, we have no background, so we can see what’s underneath our element. Adding a border means we see that border between the padding limit and the border limit. But, if the padding is non-zero, we still have a transparent area between the content limit and the padding limit.

highlighting the border, padding, and content area via dev tools

We can test it live with this Pen:

See the Pen space between border and background by Ana Tudor (@thebabydino) on CodePen.

We can make things more interesting by adding a drop-shadow() filter that gives the whole thing a yellowish glow:

See the Pen space between border and background v2 by Ana Tudor (@thebabydino) on CodePen.

Prefix reminder: I’ve seen a lot of resources adding -moz- and -ms- prefixes for CSS filters. Please, don’t do that! CSS filters have been unprefixed in Firefox ever since they were first implemented (Firefox 34, autumn of 2014) and now they’ve landed in Edge behind a flag – also unprefixed! So CSS filters never needed the -moz- or -ms- prefixes, it’s completely useless to add them, the only thing they do is bloat the stylesheet.

We can also get a cool looking effect if we use a gradient for both the background-image and the border-image. We’ll make a gradient that starts from a solid orange/red at the top, then fades down to complete transparency. Since only the shades are different and otherwise the gradients used are identical, we create a Sass function.

@function fade($c) {
  return linear-gradient($c, rgba($c, 0) 80%);
}

div {
  border: solid 0.125em;
  border-image: fade(#be4c39) 1;
  padding: 0.125em;
  background: fade(#e18728) content-box;
}

We can test it live in this Pen:

See the Pen space between border and background v3 by Ana Tudor (@thebabydino) on CodePen.

This approach of using the padding to create the space between the background and the border is not the best unless we only have a short text in the middle. If we have a lot more text… well, it looks crappy.

See the Pen space between border and background v4 by Ana Tudor (@thebabydino) on CodePen.

The problem is that the text starts right from the edge of the orange background and we cannot add a padding because we’ve already used it for the transparent gap. We could add an extra element… or we could use box-shadow!

box-shadow can take 2, 3, or 4 length values. The first one is the x offset (determining how much to move the shadow to the right), the second one is the y offset (how much to move the shadow down), the third is the blur radius (determining how blurry the edge of the shadow is) and the fourth is the spread radius (determining how much the shadow expands in all directions).

The following interactive demo allows playing with these values — click any of them and a popup with a slider shows up.

See the Pen how `box-shadow` works v2 by CSS-Tricks (@css-tricks) on CodePen.

Note that, while the blur radius has to be always greater or equal to zero, the offsets and the spread radius can be negative. A negative offset simply moves the shadow in the negative direction of its axis (left or up). A negative spread radius means the shadow shrinks instead of expanding.

Another important thing to notice here — because it’s convenient for our use case — is that that the box-shadow is never visible underneath the space occupied by the border-box, not even when that space is (semi)transparent.

If we keep the offsets and the blur radius to zero, but give the spread radius a positive value, then we get what looks like a second solid border of equal width in all directions, starting from the limit of the actual border and going outwards.

emulate border with box-shadow

Note that this kind of border doesn’t necessarily need to have equal width in all directions. We can give it different border widths by tweaking the offset values and the spread radius, with the restriction that the sum of the widths of the horizontal borders equals the sum of the widths of the vertical borders. Using multiple shadows could help us get past this restriction if we don’t need the border to be semitransparent, but this would be the kind of solution that complicates things instead of simplifying them.

Returning to our demo, we use the spread of the box-shadow to fake the border, use the actual border to create the transparent gap, set background-clip to padding-box, and let the padding do its job:

border: solid 1em transparent;
padding: 1em;
box-shadow: 0 0 0 1em #be4c39;
background: #e18728 padding-box;
highlighting the border, padding, and content area via dev tools

It can be seen in action in the following Pen:

See the Pen space between border and background v5 by Ana Tudor (@thebabydino) on CodePen.

We could also fake a border by emulating an inset shadow. In this case, it starts at the padding limit (the limit between the padding and the border areas) and goes inwards as much as the spread specifies:

emulate a second border with an inset box-shadow

Because we can have multiple box shadows, we can fake multiple borders this way. Let’s take the case where we have two, one of them an inset one. If the actual border of the element is non-zero and transparent and the background-clip is set to padding-box, then we get a fake double border with a transparent area (the actual border area) between its inner and outer components. Note that this requires also increasing the padding to compensate for the space taken by the inset box-shadow.

border: solid 1em transparent;
padding: 2em; // increased from 1em to 2em to compensate for inner "border"
box-shadow: 
  0 0 0 0.25em #be4c39 /* outer "border" */,
  inset 0 0 0 1em #be4c39 /* inner "border" */;
background: #e18728 padding-box;

It can be tested live in this Pen, where we also have a drop-shadow() filter for a glow effect:

See the Pen space between border and background v6 by Ana Tudor (@thebabydino) on CodePen.

Single element (no pseudos) target with smooth edges

Let’s say we want to get a target like the one below, with the restriction that we can only use one element and no pseudo-elements.

the target we want to CSS

The first idea would be to use a repeating-radial-gradient. Our target is structured something like this:

target structure illustration

So half the target would be 9 units, meaning the horizontal and vertical dimensions of the target are each 18 units. Our repeating radial gradient is black for the first unit, then transparent until the third unit, then black again and then transparent again… and this sounds like a repetition. Except we only have one unit from 0 to 1, the first time we have a black region, but then the second time we have a black region, it goes from 3 to 5 — that’s two units! So… we shouldn’t start from 0 there, but from -1 instead, right? Well, that should work, according to the spec.

$unit: 1em;

background: repeating-radial-gradient(
  #000 (-$unit), #000 $unit, 
  transparent $unit, transparent 3*$unit
);

This Pen illustrates the idea:

See the Pen repeating-radial-gradient for target by Ana Tudor (@thebabydino) on CodePen.

The first problem here is that IE has a different opinion on how this should work.

repeating-radial-gradient with negative first stop: expected result (left) vs. IE (right)

Luckily, this has been fixed in Edge, but if IE support is needed, it’s still a problem. One that we can solve by using a plain radial gradient because we don’t need that many circles anyway. It’s more code, but it’s not that bad…

background: radial-gradient(
  #000 $unit, transparent 0, 
  transparent 3*$unit, #000 0, 
  #000 5*$unit, transparent 0, 
  transparent 7*$unit, #000 0, 
  #000 9*$unit, transparent 0
);

We can see it in action in this Pen:

See the Pen radial-gradient for target by Ana Tudor (@thebabydino) on CodePen.

The circles are now distributed the same way in all browsers, but we still have another problem: the edges may not be as far from smooth as in the original image in IE/ Edge, but they look ugly in Firefox and Chrome!

original (top left) vs. IE/ Edge (top right) vs. Firefox (bottom left) vs. Chrome (bottom right)

We could use the non-sharp transition trick:

background: radial-gradient(
  #000 calc(#{$unit} - 1px), 
  transparent $unit, 
  transparent calc(#{3*$unit} - 1px), 
  #000 3*$unit, 
  #000 calc(#{5*$unit} - 1px), 
  transparent 5*$unit, 
  transparent calc(#{7*$unit} - 1px), 
  #000 7*$unit, 
  #000 calc(#{9*$unit} - 1px), 
  transparent 9*$unit
);

Live test:

See the Pen radial-gradient for target v2 by Ana Tudor (@thebabydino) on CodePen.

Well, this improves things in IE (where the result already looked good) and Firefox, but the edges still look ugly in Chrome.

original (top left) vs. IE (top right) vs. Firefox (bottom left) vs. Chrome (bottom right)

Maybe radial gradients are not the best solution after all. What if we were to adapt the background-clip and box-shadow solution from the previous section to this problem? We can use an outer box-shadow for the outer circle and an inset one for the inner circle. The space between them gets taken by a transparent border. We also set background-clip to content-box and give the element enough padding so that we have a transparent area between the central disc and the inner circle.

border: solid 2*$unit transparent;
padding: 4*$unit;
width: 2*$unit; height: 2*$unit;
border-radius: 50%;
box-shadow: 
  0 0 0 2*$unit #000, 
  inset 0 0 0 2*$unit #000;
background: #000 content-box;

We can see it working in the following pen, no jagged edges and no trouble:

See the Pen CSS target with smooth edges by CSS-Tricks (@css-tricks) on CodePen.

Real life-like looking controls

I first got this idea when trying to style a range input’s thumb, track and, for non-WebKit browsers, progress (fill). Browsers provide pseudo-elements for these components.

For the track, we have -webkit-slider-runnable-track, -moz-range-track and -ms-track. For the thumb, -webkit-slider-thumb, -moz-range-thumb and -ms-thumb. And for the progress/ fill, we have -moz-range-progress, -ms-fill-lower (both to the left of the thumb) and -ms-fill-upper (to the right of the thumb). WebKit browsers don’t provide a pseudo-element that would allow styling the part before the thumb different from the part after.

These look inconsistent and ugly, but what’s even uglier is that we cannot list all the browser versions for the same component together to style them. Something like this won’t work:

input[type='range']::-webkit-slider-thumb, 
input[type='range']::-moz-range-thumb, 
input[type='range']::-ms-thumb { /* styles here */ }

We have to always write them like this:

input[type='range']::-webkit-slider-thumb { /* styles here */ }
input[type='range']::-moz-range-thumb { /* styles here */ } 
input[type='range']::-ms-thumb { /* styles here */ }

Which looks like a very WET style of writing code and it actually is in a lot of cases—though given the many browser inconsistencies when it comes to sliders, it’s also useful for leveling things across the field. My solution to this was to use a thumb() mixin and maybe give it arguments for handling inconsistencies. Something like this, for example:

@mixin thumb($flag: false) {
  /* styles */
	
  @if $flag { /* more styles */ }
}

input[type='range'] {
  &::-webkit-slider-thumb { @include thumb(true); }
  &::-moz-range-thumb { @include thumb(); } 
  &::-ms-thumb { @include thumb(); }
}

But let’s go back to how we can style things. We can only add pseudo-elements to these components for Chrome/ Opera, which means that, in reproducing their look, we have to get as close as possible to it without relying on pseudo-elements. This leaves backgrounds, borders, shadows, filters on the thumb element itself.

Let’s see a few examples!

Soft plastic control

For a visual example, think something like the thumb of the slider below:

slider with soft plastic thumb

First thought would be it’s as simple as a gradient background and a gradient for border-image, then just drop a box-shadow there and that’s it for such a control:

border: solid 0.375em;
border-image: linear-gradient(#fdfdfd, #c4c4c4) 1;
box-shadow: 0 0.375em 0.5em -0.125em #808080;
background: linear-gradient(#c5c5c5, #efefef);

And this does work (using a button element instead of the slider thumb to simplify things):

See the Pen soft plastic button (square) by Ana Tudor (@thebabydino) on CodePen.

Except our control is round, not square. We’d just need to set border-radius: 50% on it then, right? Well… that doesn’t work because we’re using border-image, which makes border-radius be ignored on the element itself, though, funny enough, it still gets applied on the box-shadow, if there is one.

What should we do then? Well, use background-clip! We first give the element a non-zero padding, no border and make it round with border-radius: 50%. Then we layer two gradient backgrounds, the top one being restricted to the content-box (note the clipping is being applied as part of the background shorthand). Finally, we add two box shadows, the first one being a dark one that creates the shadow underneath the control and the second being an inset one, that should darken a bit the bottom and laterals of the control’s outer part.

border: none; /* makes border-box ≡ padding-box */
padding: .375em;
border-radius: 50%;
box-shadow: 0 .375em .5em -.125em #808080, 
      inset 0 -.25em .5em -.125em #bbb;
background: 
  linear-gradient(#c5c5c5, #efefef) content-box, 
  linear-gradient(#fdfdfd, #c4c4c4);

The final result can be seen in this Pen:

See the Pen soft plastic button (really round!) by Ana Tudor (@thebabydino) on CodePen.

Matte control

Something like the thumb of the slider in the following image:

slider with matte thumb

This looks pretty similar to the previous case, except now we have a lighter line at the top and an inset shadow for the middle part. If we use an outer box-shadow to create the lighter line, the whole thing wouldn’t be round anymore unless we also decrease its height to compensate for the shadow. That would mean we need to do more computations to determine the position of the inner part. If we use an inset one instead, then we can’t use that for the dark shadow of the inner part. However, we could emulate that with a radial-gradient that gets conveniently sized, positioned, and clipped to the content-box. This means the same strategy as in the previous case, except we have an extra radial-gradient layered on top of the other backgrounds. The actual background-size of the radial gradient is larger than the content-box so we can shift it down without bringing its top edge within the content limit.

border: none; /* makes border-box ≡ padding-box */
padding: .625em;
width: 1.75em; height: 1.75em;
border-radius: 50%;
box-shadow: 
  0 1px .125em #444 /* dark lower shadow */, 
  inset 0 1px .125em #fff /* light top hint */;
background: 
  /* inner shadow effect */
  radial-gradient(transparent 35%, #444) 
    50% calc(50% + .125em) content-box, 
  
  /* inner background */
  linear-gradient(#bbb, #bbb) content-box, 
  
  /* outer background */
  linear-gradient(#d0d3d5, #d2d5d7);
background-size: 
  175% 175% /* make radial-gradient bg larger */, 
  100% 100%, 100% 100%;

We can see it live in this Pen:

See the Pen matte button by Ana Tudor (@thebabydino) on CodePen.

3D control

For example, the thumb of the following slider:

slider with 3D thumb

This one is a bit more complex and requires that the three boxes (the content-box, the padding-box, and the border-box) are all different, so that we can layer backgrounds and use background-clip to get the desired effect.

So for the main part of the slider, we have a gradient background clipped to the content-box layered on top of another clipped to the padding-box, both of them over a third linear-gradient clipped to border-box. We also make use of inset box-shadow to highlight the padding limit (between the padding and the border):

border: solid .25em transparent;
padding: .25em;
border-radius: 1.375em;
box-shadow: 
  inset 0 1px 1px rgba(#f7f7f7, .875) /* top */, 
  inset 0 -1px 1px rgba(#bbb, .75) /* bottom */;
background: 
  linear-gradient(#9ea1a6, #fdfdfe) content-box, 
  linear-gradient(#fff, #9c9fa4) padding-box, 
  linear-gradient(#eee, #a4a7ab) border-box;

The result for this part can be seen in this Pen:

See the Pen 3D button by Ana Tudor (@thebabydino) on CodePen.

Now what’s left is the little round part. This is one case where I felt a pseudo-element was really needed. I did end up taking that route for Blink, but managed to get a decent-looking fallback for the rest of the browsers by layering two radial gradients on top of the linear ones:

See the Pen 3D button #2 by Ana Tudor (@thebabydino) on CodePen.

We might be able to get even closer to what we want with better chosen shades, extra radial gradients, or even using background-blend-mode, but I don’t have the artistic sense needed for something like that.

With a pseudo-element, it’s a lot easier to get the desired result — we first need to position and size it properly, make it round with border-radius: 50%. Then we give it a padding, no border, and use two gradients for the background, the top one being a radial one clipped to the content-box:

padding: .125em;
background: 
  radial-gradient(circle at 50% 10%, 
      #f7f8fa, #9a9b9f) content-box, 
  linear-gradient(#ddd, #bbb);

The result for this can be seen in the following Pen:

See the Pen 3D button #3 by Ana Tudor (@thebabydino) on CodePen.

For the actual slider thumb, I used the same thumb background with the radial gradients on top everywhere, and added the pseudo for Blink right on top of the radial gradients. This is because thumb slider styles in Safari get applied via ::-webkit-slider-thumb, but Safari doesn’t support pseudo-elements on the thumb (or track). So if I were to remove the fallback from the styles applied to ::-webkit-slider-thumb, then Safari wouldn’t display the round part at all.

Illusion of depth

The track of the following slider illustrates this idea:

slider with track having depth

Just like before, we do this by giving the element a non-zero transparent border, a padding, and layering backgrounds with different background-clip values (remember that those with content-box values need to be on top of those with padding-box values, which need to be on top of those with border-box values). In this case, we have a lighter linear-gradient to cover the border area, a darker one plus a couple of radial ones which we make smaller and non-repeating to darken the ends even further for the padding area, and a really dark one for the content area. Then we give this a border-radius equal to at least half the height of the content area plus twice the padding and twice the border. We also add an inset box-shadow to subtly highlight the padding limit.

border: solid .375em transparent;
padding: 1em 3em;
width: 15.75em; height: 1.75em;
border-radius: 2.25em;
background: 
  linear-gradient(#090909, #090909) content-box, 
  radial-gradient(at 5% 40%, #0b0b0b, transparent 70%) 
    no-repeat 0 35% padding-box /* left */, 
  radial-gradient(at 95% 40%, #111, transparent 70%) 
    no-repeat 100% 35% padding-box /* right */, 
  linear-gradient(90deg, #3a3a3a, #161616) padding-box,
  linear-gradient(90deg, #2b2d2c, #2a2c2b) border-box;
background-size: 100%, 9em 4.5em, 4.5em 4.5em, 100%, 100%;

But there’s a problem with this, and it can be seen in the following Pen:

See the Pen illusion of depth by Ana Tudor (@thebabydino) on CodePen.

Due to the way border-radius works — the radius for the content area being the one we specify minus the border-width minus the padding, which ends up being negative — there is no rounding for the corners of the content area. Well, we can fix this! We can emulate the shape we want by using both linear and radial gradients for the content area.

The way we do this is by first making sure the width of our content area is a multiple of its height (in our case, 15.75em = 9*1.75em). We first layer a non-repeating linear-gradient positioned in the middle that covers the entire height of the content area vertically, but leaves a space of half the content area height at both the left and the right ends. On top of this we add a radial-gradient with a background-size equal to the content area height both horizontally and vertically.

See the Pen illusion of depth #2 by Ana Tudor (@thebabydino) on CodePen.

Metallic controls

For example, something like the button illustrated below:

metallic control

This is a bit more complex, so let’s break it down. First of all, we make the button circular by giving it equal width and height and setting border-radius: 50%. Then we make sure it has box-sizing: border-box, so that the border-width and the padding go inwards (get subtracted from the dimensions we have set). Now the next logical step is to give it a transparent border and a padding. So far, this is what we have:

$d-btn: 27em; /* control diameter */
$bw: 1.5em; /* border-width */

button {
  box-sizing: border-box;
  border: solid $bw transparent;
  padding: 1.5em;
  width: $d-btn; height: $d-btn;
  border-radius: 50%;
}

It doesn’t look like anything yet and that’s because the entire look is achieved with the help of just two properties we haven’t added in yet: box-shadow and background.

See the Pen metallic control – step 0 by Ana Tudor (@thebabydino) on CodePen.

Before doing anything else, let’s deconstruct the control a bit:

deconstructing the metallic control

Starting from the outer part and going inwards, we have:

  • a thick outer ring with LEDs
  • a thin inner ring
  • a perforated area (that includes the cyan glow around the following inner part)
  • a big central part

The thick outer ring is the border area, the central part is the content area and everything in between (the thin inner ring and the perforated part) is the padding area. We create the thin inner ring with inset box shadows:

box-shadow: 
  /* discrete dark shadow to act as separator from outer ring */
  inset 0 0 1px #666, 

  /* darker top area */
  inset 0 1px .125em #8b8b8b, 
  inset 0 2px .25em #a4a2a3, 

  /* darker bottom area */
  inset 0 -1px .125em #8b8b8b, 
  inset 0 -2px .25em #a4a2a3, 

  /* the base circular strip for the inner ring */
  inset 0 0 0 .375em #cdcdcd;

We also add two more outer box shadows, one for the lighter top highlight of the outer ring and the second for the discrete dark shadow below the control, so we now have:

box-shadow: 
  0 -1px 1px #eee, 
  0 2px 2px #1d1d1d, 
  inset 0 0 1px #666, 
  inset 0 1px .125em #8b8b8b, 
  inset 0 2px .25em #a4a2a3, 
  inset 0 -1px .125em #8b8b8b, 
  inset 0 -2px .25em #a4a2a3, 
  inset 0 0 0 .375em #cdcdcd;

Still not much, but it’s more than before:

See the Pen metallic control – step 1 by Ana Tudor (@thebabydino) on CodePen.

Now we have to layer three types of backgrounds, from top to bottom: limited to the content-box (creating the central area), limited to the padding-box (creating the perforated area and cyan glow) and limited to the border-box (creating the thick outer ring and LEDs).

We start with the central area, where we have some discrete circular lines created with three repeating radial gradients stacked one on top of the other, with the values of the stops based on the cicada principle and conic reflections created with a conic gradient. Note that conic gradients are not yet supported in any browser so we need to use a polyfill at this point.

background:
  /* ======= content-box ======= */
  /* circular lines - 13, 19, 23 being prime numbers */
  repeating-radial-gradient(
      rgba(#e4e4e4, 0) 0, 
      rgba(#e4e4e4, 0) 23px, 
      rgba(#e4e4e4, .05) 25px, 
      rgba(#e4e4e4, 0) 27px) content-box, 
  repeating-radial-gradient(
      rgba(#a6a6a6, 0) 0, 
      rgba(#a6a6a6, 0) 13px, 
      rgba(#a6a6a6, .05) 15px, 
      rgba(#a6a6a6, 0) 17px) content-box, 
  repeating-radial-gradient(
      rgba(#8b8b8b, 0) 0, 
      rgba(#8b8b8b, 0) 19px, 
      rgba(#8b8b8b, .05) 21px, 
      rgba(#8b8b8b, 0) 23px) content-box, 
  /* conic reflections */
  conic-gradient(/* random variations of some shades of grey */
      #cdcdcd, #9d9d9d, #808080, 
      #bcbcbc, #c4c4c4, #e6e6e6, 
      #dddddd, #a1a1a1, #7f7f7f, 
      #8b8b8b, #bfbfbf, #e3e3e3, 
      #d2d2d2, #a6a6a6, #858585, 
      #8d8d8d, #c0c0c0, #e5e5e5, 
      #d6d6d6, #9e9e9e, #828282, 
      #8f8f8f, #bdbdbd, #e3e3e3, #cdcdcd) 
    content-box;

Now this is finally starting to look like something!

See the Pen metallic control – step 2 by Ana Tudor (@thebabydino) on CodePen.

We move on to the perforated area. The cyan glow is just a radial gradient to transparency in the outer part, while the perforations are based on the Carbon fibre pattern from the gallery Lea Verou put together some five years ago – still damn useful for artistically challenged people such as myself.

$d-hole: 1.25em; /* perforation diameter*/
$r-hole: .5*$d-hole; /* perforation radius */

background: 
  /* ======= padding-box ======= */
  /* cyan glow */
  radial-gradient(
      #00d7ff 53%, transparent 65%) padding-box, 
  /* holes */
  radial-gradient(
      #272727 20%, transparent 25%) 
    0 0 / #{$d-hole} #{$d-hole} 
    padding-box,
  radial-gradient(
      #272727 20%, transparent 25%) 
    $r-hole $r-hole / #{$d-hole} #{$d-hole} 
    padding-box,
  radial-gradient(#444 20%, transparent 28%) 
    0 .125em / #{$d-hole} #{$d-hole} 
    padding-box,
  radial-gradient(#444 20%, #3d3d3d 28%) 
    #{$r-hole} #{$r-hole + .125em} / #{$d-hole} #{$d-hole} 
    padding-box

It looks like we’re getting close to something decent looking:

See the Pen metallic control – step 3 by Ana Tudor (@thebabydino) on CodePen.

The basic thick outer ring (without the LEDs) is created with a single conic gradient:

conic-gradient(
  #b5b5b5, #8d8d8d, #838383, 
  #ababab, #d7d7d7, #e3e3e3, 
  #aeaeae, #8f8f8f, #878787, 
  #acacac, #d7d7d7, #dddddd, 
  #b8b8b8, #8e8e8e, #848484, 
  #a6a6a6, #d8d8d8, #e3e3e3, 
  #8e8e8e, #868686, #a8a8a8, 
  #d5d5d5, #dedede, #b5b5b5) border-box;

We now have a metallic control!

See the Pen metallic control – step 4 by Ana Tudor (@thebabydino) on CodePen.

It has no LEDs at this point, so let’s fix that!

Every LED is made up of two non-repeating radial gradients stacked one on top of the other. The top one is the actual LED, while the bottom one, slightly offset vertically, creates the lighter highlight in the lower part of the LED. It’s pretty much the same effect used for the holes in the perforated area. The bottom gradient is always the same, but the top one differs depending on whether the LEDs are on or off.

We take the LEDs to be on up to the $k-th one. So up to the point we use the cyan variation for the top gradient, while after that we use the grey one.

We have 24 LEDs that are positioned on a circle passing through the middle of its border area. So its radius is the radius of the control minus half the border width.

We generate all these gradients with Sass. We first create an empty list of gradients, then we loop and, for every iteration, we add two gradients to the list. Their positions are computed so they’re on the previously mentioned circle. The first gradient depends on the loop index, while the second one is always the same (only at another position on the circle).

$d-btn: 27em;
$bw: 1.5em;
$r-pos: .5*($d-btn - $bw);
$n-leds: 24;
$ba-led: 360deg/$n-leds;
$d-led: 1em;
$r-led: .5*$d-led;
$k: 7;
$leds: ();

@for $i from 0 to $n-leds {
  $a: $i*$ba-led - 90deg;
  $x: .5*$d-btn + $r-pos*cos($a) - $r-led;
  $y: .5*$d-btn + $r-pos*sin($a) - $r-led;
  $leds: $leds, 
    if($i < $k, 
      (radial-gradient(circle, #01d6ff, 
          #178b98 .5*$r-led, 
          rgba(#01d6ff, .35) .7*$r-led, 
          rgba(#01d6ff, 0) 1.3*$r-led) no-repeat 
        #{$x - $r-led} #{$y - $r-led} / 
        #{2*$d-led} #{2*$d-led} border-box), 
      (radial-gradient(circle, #898989, 
          #4d4d4d .5*$r-led, #999 .65*$r-led, 
          rgba(#999, 0) .7*$r-led) no-repeat 
        $x $y / #{$d-led} #{$d-led} border-box)
    ), 
    radial-gradient(circle, 
        rgba(#e8e8e8, .5) .5*$r-led, 
        rgba(#e8e8e8, 0) .7*$r-led) no-repeat 
      $x ($y + .125em) / #{$d-led} #{$d-led} 
      border-box;
}

The final result can be seen in this Pen:

See the Pen 1 element (no pseudos) metallic control by Ana Tudor (@thebabydino) on CodePen.

Shadows in a perpendicular plane

Consider the case of controls being in the vertical plane of the screen, for which we want to have a shadow in a horizontal plane below. Something like in the following image:

controls with shadow in a horizontal plane below them

What we want is to recreate this effect using just one element and no pseudo-elements.

Layering backgrounds with different background-clip and background-origin values does the trick in this case as well. We create the actual button with two backgrounds, the one on top clipped to the content-box and the one under it clipped to the padding-box and use a radial-gradient() background with background-clip and background-origin set to border-box to create the shadow.

The basic styling is pretty similar to that of the metallic control in the previous section:

$l: 6.25em;
$bw: .1*$l;

border: solid $bw transparent;
padding: 3px;
width: $l; height: $l;
border-radius: 1.75*$bw;

We give it a thickish transparent border all around, so that we have enough space to recreate that shadow in the bottom border area. We do this for all borders, not just for the bottom one because we want the same kind of symmetrical rounding for all the corners of the padding-box (if you need a refresher of how this works, check out Lea Verou’s excellent border-radius talk).

See the Pen border-width & padding-box corner rounding by Ana Tudor (@thebabydino) on CodePen.

The first background from the top is a conic-gradient() one to create the conic metal reflections. This one is clipped to the content-box. Right underneath it, we have a simple linear-gradient() clipped to the padding-box. We use three inset box shadows to make this second background less flat – add another shade all around with a zero blur, positive spread shadow, make it lighter at the top with a semitransparent white shadow and darker at the bottom with a semitransparent black shadow.

box-shadow: 
  inset 0 0 0 1px #eedc00, 
  inset 0  1px 2px rgba(#fff, .5), 
  inset 0 -1px 2px rgba(#000, .5);
background: 
  conic-gradient(
      #edc800, #e3b600, #f3cf00, #ffe800, 
      #ffe900, #ffeb00, #ffe000, #ebc500, 
      #e0b100, #f1cc00, #fcdc00, #ffe500, 
      #fad900, #eec200, #e7b900, #f7d300, 
      #ffe800, #ffe300, #f5d100, #e6b900, 
      #e3b600, #f4d000, #ffe400, #ebc600, 
      #e3b600, #f6d500, #ffe900, #ffe90a, 
      #edc800) content-box, 
  linear-gradient(#f6d600, #f6d600) padding-box

This gives us the metallic button (without the shadow yet):

See the Pen metallic button – no shadow by Ana Tudor (@thebabydino) on CodePen.

For the shadow, layer a third background for which we set both background-clip and background-origin to border-box. This background is a non-repeating radial-gradient() whose position we attach to the bottom (and horizontally in the middle) and which we shrink vertically so that it fits into that bottom border area and even leaves a bit of space – so we take it for example to be something like .75 of the border-width.

radial-gradient(rgba(#787878, .9), rgba(#787878, 0) 70%) 
  50% bottom / 80% .75*$bw no-repeat border-box

And this is it! You can play with the buttons in the following Pen:

See the Pen metallic buttons with shadow (no pseudos) by Ana Tudor (@thebabydino) on CodePen.


Background-clip certainly has its use cases! Particularly when layering multiple effects around the edges of elements.


The `background-clip` Property and its Use Cases originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/the-backgound-clip-property-and-use-cases/feed/ 12 237034
background-clip https://css-tricks.com/almanac/properties/b/background-clip/ https://css-tricks.com/almanac/properties/b/background-clip/#comments Tue, 17 Feb 2015 22:39:07 +0000 http://css-tricks.com/?page_id=196161 background-clip lets you control how far a background image or color extends beyond an element’s padding or content.

.frame {
  background-clip: padding-box;
}

Values

  • border-box is the default value. This allows the background to extend all the way to the


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

]]>
background-clip lets you control how far a background image or color extends beyond an element’s padding or content.

.frame {
  background-clip: padding-box;
}

Values

  • border-box is the default value. This allows the background to extend all the way to the outside edge of the element’s border.
  • padding-box clips the background at the outside edge of the element’s padding and does not let it extend into the border.
  • content-boxclips the background at the edge of the content box.
  • inherit applies the background-clip setting of the parent to the selected element.

Demos

background-clip is best explained in action, so we’ve put together two demos to show how it works.

In the first demo, each div has one paragraph inside it. The paragraph is the div’s content. Each div has a yellow background and a 5 pixel, semi-transparent light blue border.

By default, or with background-clip: border-box set, the yellow background extends all the way to the outside edge of the border. Notice how the border looks like it’s green because of the yellow background beneath it.

When the background-clip is changed to padding-box, the background color stops where the element’s padding ends. Notice that the border is blue because the background isn’t allowed to bleed into the border.

With background-clip: content-box, the background color only applies itself to the div’s content, in this case the single paragraph element. The div’s padding and border don’t have a background color. But, there’s one little detail worth mentioning: the color does extend into the content’s margin. Check out the differences between the first and second examples with content-box.

In the first content-box example, the browser’s default margins are applied to the paragraph, and the background clips after the margin. In the second example, the margin is set to 0 in the CSS, and the background is clipped at the edge of the text.

This next interactive shows background-clip with a background image. The content in this demo is a smaller empty div.

This demo also applies background-size: cover and background-repeat: no-repeat in addition to background-clip to control the background image, which would otherwise repeat until clipping.

Text

There is a vendor-prefixed version of this that works for clipping a background to text. In order to see that work, the text will also need to be transparent. Fortunately, there is another vendor-prefixed text color property that can effectively override color, making it safe to use as it can have a fallback:

.background-clip-text {
  
  /* if we can clip, do it */
  -webkit-text-fill-color: transparent;
  -webkit-background-clip: text;

  /* what will show through the text
      ~ or ~
     what will be the background of that element */
  background: whatever;

  /* fallback text color
      ~ or ~
     the actual color of text, so it better work on the background */
  color: red;
 
}

Firefox, Chrome, and Safari support this.

Browser support

IEEdgeFirefoxChromeSafariOpera
9+12+22+21+5.1+15+
iOS SafariAndroid ChromeAndroid FirefoxAndroid BrowserOpera Mobile
AllAllAll3+62+
Source: caniuse

More information


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

]]>
https://css-tricks.com/almanac/properties/b/background-clip/feed/ 2 196161