mix-blend-mode – CSS-Tricks https://css-tricks.com Tips, Tricks, and Techniques on using Cascading Style Sheets. Thu, 08 Dec 2022 15:36:22 +0000 en-US hourly 1 https://wordpress.org/?v=6.2.2 https://i0.wp.com/css-tricks.com/wp-content/uploads/2021/07/star.png?fit=32%2C32&ssl=1 mix-blend-mode – CSS-Tricks https://css-tricks.com 32 32 45537868 Animated Background Stripes That Transition on Hover https://css-tricks.com/animated-background-stripes-transition-hover/ https://css-tricks.com/animated-background-stripes-transition-hover/#comments Thu, 08 Dec 2022 15:36:21 +0000 https://css-tricks.com/?p=375697 How often to do you reach for the CSS background-size property? If you’re like me — and probably lots of other front-end folks — then it’s usually when you background-size: cover an image to fill the space of an entire …


Animated Background Stripes That Transition on Hover originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
How often to do you reach for the CSS background-size property? If you’re like me — and probably lots of other front-end folks — then it’s usually when you background-size: cover an image to fill the space of an entire element.

Well, I was presented with an interesting challenge that required more advanced background sizing: background stripes that transition on hover. Check this out and hover it with your cursor:

There’s a lot more going on there than the size of the background, but that was the trick I needed to get the stripes to transition. I thought I’d show you how I arrived there, not only because I think it’s a really nice visual effect, but because it required me to get creative with gradients and blend modes that I think you might enjoy.

Let’s start with a very basic setup to keep things simple. I’m talking about a single <div> in the HTML that’s styled as a green square:

<div></div>
div {
  width: 500px;
  height: 500px;
  background: palegreen;
}
Perfect square with a pale green background color.

Setting up the background stripes

If your mind went straight to a CSS linear gradient when you saw those stripes, then we’re already on the same page. We can’t exactly do a repeating gradient in this case since we want the stripes to occupy uneven amounts of space and transition them, but we can create five stripes by chaining five backgrounds on top of our existing background color and placing them to the top-right of the container:

div {
  width: 500px;
  height: 500px;
  background: 
    linear-gradient(black, black) top right,
    linear-gradient(black, black) top 100px right,
    linear-gradient(black, black) top 200px right,
    linear-gradient(black, black) top 300px right,
    linear-gradient(black, black) top 400px right, 
    palegreen;
}

I made horizontal stripes, but we could also go vertical with the approach we’re covering here. And we can simplify this quite a bit with custom properties:

div {
  --gt: linear-gradient(black, black);
  --n: 100px;

  width: 500px;
  height: 500px;
  background: 
    var(--gt) top right,
    var(--gt) top var(--n) right,
    var(--gt) top calc(var(--n) * 2) right,
    var(--gt) top calc(var(--n) * 3) right,
    var(--gt) top calc(var(--n) * 4) right, 
    palegreen;
}

So, the --gt value is the gradient and --n is a constant we’re using to nudge the stripes downward so they are offset vertically. And you may have noticed that I haven’t set a true gradient, but rather solid black stripes in the linear-gradient() function — that’s intentional and we’ll get to why I did that in a bit.

One more thing we ought to do before moving on is prevent our backgrounds from repeating; otherwise, they’ll tile and fill the entire space:

div {
  --gt: linear-gradient(black, black);
  --n: 100px;

  width: 500px;
  height: 500px;
  background: 
    var(--gt) top right,
    var(--gt) top var(--n) right,
    var(--gt) top calc(var(--n) * 2) right,
    var(--gt) top calc(var(--n) * 3) right,
    var(--gt) top calc(var(--n) * 4) right, 
    palegreen;
  background-repeat: no-repeat;
}

We could have set background-repeat in the background shorthand, but I decided to break it out here to keep things easy to read.

Offsetting the stripes

We technically have stripes, but it’s pretty tough to tell because there’s no spacing between them and they cover the entire container. It’s more like we have a solid black square.

This is where we get to use the background-size property. We want to set both the height and the width of the stripes and the property supports a two-value syntax that allows us to do exactly that. And, we can chain those sizes by comma separating them the same way we did on background.

Let’s start simple by setting the widths first. Using the single-value syntax for background-size sets the width and defaults the height to auto. I’m using totally arbitrary values here, so set the values to what works best for your design:

div {
  --gt: linear-gradient(black, black);
  --n: 100px;

  width: 500px;
  height: 500px;
  background: 
    var(--gt) top right,
    var(--gt) top var(--n) right,
    var(--gt) top calc(var(--n) * 2) right,
    var(--gt) top calc(var(--n) * 3) right,
    var(--gt) top calc(var(--n) * 4) right, 
    palegreen;
  background-repeat: no-repeat;
  background-size: 60%, 90%, 70%, 40%, 10%;
}

If you’re using the same values that I am, you’ll get this:

Doesn’t exactly look like we set the width for all the stripes, does it? That’s because of the auto height behavior of the single-value syntax. The second stripe is wider than the others below it, and it is covering them. We ought to set the heights so we can see our work. They should all be the same height and we can actually re-use our --n variable, again, to keep things simple:

div {
  --gt: linear-gradient(black, black);
  --n: 100px;

  width: 500px;
  height: 500px;
  background: 
    var(--gt) top right,
    var(--gt) top var(--n) right,
    var(--gt) top calc(var(--n) * 2) right,
    var(--gt) top calc(var(--n) * 3) right,
    var(--gt) top calc(var(--n) * 4) right, 
    palegreen;
    background-repeat: no-repeat;
    background-size: 60% var(--n), 90% var(--n), 70% var(--n), 40% var(--n), 10% var(--n); // HIGHLIGHT 15
}

Ah, much better!

Adding gaps between the stripes

This is a totally optional step if your design doesn’t require gaps between the stripes, but mine did and it’s not overly complicated. We change the height of each stripe’s background-size a smidge, decreasing the value so they fall short of filling the full vertical space.

We can continue to use our --n variable, but subtract a small amount, say 5px, using calc() to get what we want.

background-size: 60% calc(var(--n) - 5px), 90% calc(var(--n) - 5px), 70% calc(var(--n) - 5px), 40% calc(var(--n) - 5px), 10% calc(var(--n) - 5px);

That’s a lot of repetition we can eliminate with another variable:

div {
  --h: calc(var(--n) - 5px);
  /* etc. */
  background-size: 60% var(--h), 90% var(--h), 70% var(--h), 40% var(--h), 10% var(--h);
}

Masking and blending

Now let’s swap the palegreen background color we’ve been using for visual purposes up to this point for white.

div {
  /* etc. */
  background: 
    var(--gt) top right,
    var(--gt) top var(--n) right,
    var(--gt) top calc(var(--n) * 2) right,
    var(--gt) top calc(var(--n) * 3) right,
    var(--gt) top calc(var(--n) * 4) right, 
    #fff;
  /* etc. */
}

A black and white pattern like this is perfect for masking and blending. To do that, we’re first going to wrap our <div> in a new parent container and introduce a second <div> under it:

<section>
  <div></div>
  <div></div>
</section>

We’re going to do a little CSS re-factoring here. Now that we have a new parent container, we can pass the fixed width and height properties we were using on our <div> over there:

section {
  width: 500px;
  height: 500px;
} 

I’m also going to use CSS Grid to position the two <div> elements on top of one another. This is the same trick Temani Afif uses to create his super cool image galleries. The idea is that we place both divs over the full container using the grid-area property and align everything toward the center:

section {
  display: grid;
  align-items: center;
  justify-items: center;
  width: 500px;
  height: 500px;
} 

section > div {
  width: inherit;
  height: inherit;
  grid-area: 1 / 1;
}

Now, check this out. The reason I used a solid gradient that goes from black to black earlier is to set us up for masking and blending the two <div> layers. This isn’t true masking in the sense that we’re calling the mask property, but the contrast between the layers controls what colors are visible. The area covered by white will remain white, and the area covered by black leaks through. MDN’s documentation on blend modes has a nice explanation of how this works.

To get that working, I’ll apply the real gradient we want to see on the first <div> while applying the style rules from our initial <div> on the new one, using the :nth-child() pseudo-selector:

div:nth-child(1) { 
  background: linear-gradient(to right, red, orange); 
}

div:nth-child(2)  {
  --gt: linear-gradient(black, black);
  --n: 100px;
  --h: calc(var(--n) - 5px);
  background: 
    var(--gt) top right,
    var(--gt) top var(--n) right,
    var(--gt) top calc(var(--n) * 2) right,
    var(--gt) top calc(var(--n) * 3) right,
    var(--gt) top calc(var(--n) * 4) right, 
    white;
  background-repeat: no-repeat;
  background-size: 60% var(--h), 90% var(--h), 70% var(--h), 40% var(--h), 10% var(--h);
}

If we stop here, we actually won’t see any visual difference from what we had before. That’s because we haven’t done the actual blending yet. So, let’s do that now using the screen blend mode:

div:nth-child(2)  {
  /* etc. */
  mix-blend-mode: screen;
}

I used a beige background color in the demo I showed at the beginning of this article. That slightly darker sort of off-white coloring allows a little color to bleed through the rest of the background:

The hover effect

The last piece of this puzzle is the hover effect that widens the stripes to full width. First, let’s write out our selector for it. We want this to happen when the parent container (<section> in our case) is hovered. When it’s hovered, we’ll change the background size of the stripes contained in the second <div>:

/* When <section> is hovered, change the second div's styles */
section:hover > div:nth-child(2){
  /* styles go here */
}

We’ll want to change the background-size of the stripes to the full width of the container while maintaining the same height:

section:hover > div:nth-child(2){
  background-size: 100% var(--h);
}

That “snaps” the background to full-width. If we add a little transition to this, then we see the stripes expand on hover:

section:hover > div:nth-child(2){
  background-size: 100% var(--h);
  transition: background-size 1s;
}

Here’s that final demo once again:

I only added text in there to show what it might look like to use this in a different context. If you do the same, then it’s worth making sure there’s enough contrast between the text color and the colors used in the gradient to comply with WCAG guidelines. And while we’re touching briefly on accessibility, it’s worth considering user preferences for reduced motion when it comes to the hover effect.

That’s a wrap!

Pretty neat, right? I certainly think so. What I like about this, too, is that it’s pretty maintainable and customizable. For example, we can alter the height, colors, and direction of the stripes by changing a few values. You might even variablize a few more things in there — like the colors and widths — to make it even more configurable.

I’m really interested if you would have approached this a different way. If so, please share in the comments! It’d be neat to see how many variations we can collect.


Animated Background Stripes That Transition on Hover originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/animated-background-stripes-transition-hover/feed/ 2 375697
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
Holographic Trading Card Effect https://css-tricks.com/holographic-trading-card-effect/ https://css-tricks.com/holographic-trading-card-effect/#comments Wed, 26 Oct 2022 17:05:28 +0000 https://css-tricks.com/?p=374609 Simon Goellner (@simeydotme)’s collection of Holographic Trading Cards have captured our attention.

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


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

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

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

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

To Shared LinkPermalink on CSS-Tricks


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

]]>
https://css-tricks.com/holographic-trading-card-effect/feed/ 4 374609
Image Illustration Filter https://css-tricks.com/snippets/css/image-illustration-filter/ https://css-tricks.com/snippets/css/image-illustration-filter/#comments Tue, 27 Sep 2022 18:04:54 +0000 https://css-tricks.com/?page_id=373770 I like that sorta effect where an image almost looks like it’s an oil painting or an illustration made with a thick, runny ink fountain pen. I got the idea when Scott Vandehey shared his “halftone filter” effect on the …


Image Illustration Filter originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
I like that sorta effect where an image almost looks like it’s an oil painting or an illustration made with a thick, runny ink fountain pen. I got the idea when Scott Vandehey shared his “halftone filter” effect on the Cloud Four blog.

I’d say it looks a lot like newspaper print:

The trick? We have an image chock-full of CSS filter effects and an underlying repeating radial gradient set to a super small background-size to get a dot-like effect that’s enhanced with mix-blend-mode.

The whole idea of Scott’s post is to demonstrate the power of CSS Blend Modes. So, I took the filter effects he had on the image:

img {
  /* ... */
  filter:
    grayscale(1) 
    brightness(80%) 
    contrast(150%) 
    blur(2px);
  mix-blend-mode: $blend-mode;
}

…and made a few tweaks, namely:

  • increasing the blur()a smidge (4px)
  • bumping the contrast() by an ungodly amount (3000%)
  • using screen blend mode

Here’s how that shakes out in CSS:

.painted {
  background: repeating-radial-gradient(
    circle at center,
    hsl(0 0% 15%),
    hsl(16.1 5% 55.5%);
  );
  background-size: 5px;
}

.painted img {
  filter: blur(4px) contrast(3000%) grayscale(1);
  mix-blend-mode: screen;
  width: 100%;
}

You might need to adjust that ginormous contrast() value depending on the image — something large enough to wash things out.


Image Illustration Filter originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/snippets/css/image-illustration-filter/feed/ 1 373770
How to Make a “Raise the Curtains” Effect in CSS https://css-tricks.com/css-raise-the-curtains-effect/ https://css-tricks.com/css-raise-the-curtains-effect/#comments Wed, 02 Mar 2022 15:57:32 +0000 https://css-tricks.com/?p=363882 “Raise the curtains” is what I call an effect where the background goes from dark to light on scroll, and the content on top also goes from light to dark while in a sticky position.

Here’s an example where I …


How to Make a “Raise the Curtains” Effect in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
“Raise the curtains” is what I call an effect where the background goes from dark to light on scroll, and the content on top also goes from light to dark while in a sticky position.

Here’s an example where I used the effect on a real-life project:

Want to know how it’s done? I will take you behind the curtain and show you how to raise it, with nothing but HTML and CSS.

Let’s start with the HTML

What we’re making is sort of a simplified “raise the curtain” effect like this:

Showing the raise the curtains effect from dark blue to wheat.
The background and text both change color while scrolling over the element.

I’m keeping things simple for the sake of clarity, but we can stub this out with three elements:

<div class="curtain">
  <div class="invert">
    <h2>Section title</h2>
  </div>
</div>

First, we need a container for the curtain, which we’ll give a .curtain class. Then, inside the .curtain, we have the an .invert child element that will serve as our “sticky” box. And, finally, we have the content inside this box — a good old-fashioned <h2> element for this specific example.

Let’s set up some CSS variables

There are three values we know we’ll need upfront. Let’s make CSS variables out of them so it’s easy to write them into our styles and easily change them later if we need to.

  • --minh – The height of the container
  • --color1 – The light color
  • --color2 – The dark color
:root {
  --minh: 98vh;
  --color1: wheat;
  --color2: midnightblue;
}

Time to draw the curtain

Next, we can define our .curtain element using the following techniques:

  • A linear-gradient for the “split” background
  • min-height for the extra space at the bottom of the container

We use the ::after pseudo-element to add the extra space to the bottom. This way, our “sticky” content will actually stick to the container while scrolling past the ::after element. It’s an illusion.

.curtain {
  /** create the "split" background **/
  background-image: linear-gradient(to bottom, var(--color2) 50%, var(--color1) 50%);
}

/** add extra space to the bottom (need this for the "sticky" effect) **/
.curtain::after {
  content: "";
  display: block;
  min-height: var(--minh);
}

Making sticky content

Next up, we need to make our content “sticky” in the sense that it sits perfectly inside the container as the background and text swap color values. In fact, we already gave the .curtain‘s child element an .invert class that we can use as the sticky container.

Stay with me for a moment — here’s how this is going to play out:

  • position: sticky and top define the stickiness and where it sticks.
  • mix-blend-mode: difference blends the color of the content inside the <h2> element into the .curtain‘s background gradient.
  • display: flex centers the content for presentation.
  • min-height defines the height of the container and allows for the extra space at the bottom.
  • color sets the color of the h2 heading.

Now to put that into CSS code!

.invert {
  /** make the content sticky **/
  position: sticky;
  top: 20px;

  /** blend the content with the contrast effect **/
  mix-blend-mode: difference;

  /** center the content **/
  display: flex;
  align-items: center;
  justify-content: center;
  
  /** set the minimum height of the section **/
  min-height: var(--minh);
}

h2 {
  /** set the color of the text **/
  color: var(--color1);
}

There are many things going on here, so let’s explain each one of them.

First, we have a sticky position that is self-explanatory and flexbox to help center the content. Nothing new or particularly tricky about this.

The content’s height is set using CSS variable and the value is the same height value as the .curtain::after pseudo-element.

The mix-blend-mode: difference declaration blends our content with the background. The difference value is complicated, but you might visualize it like inverted text color against the background. Here’s a nice demo from the CSS-Tricks Almanac showing off the different mix-blend-mode values:

To make the blending work, we need to set the color of our heading. In this case, we’re assigning a light color value (wheat) to the --color1 variable.

“Raise the Curtains” Demo

Gotchas

I experienced a few problems while working out the details of the “raise the curtain” effect. If you want to add images to the “sticky” content, for example, avoid using images that don’t look good when their colors are inverted. Here’s a quick demo where I made a simple SVG and transparent PNG image, and it looks good.

Another gotcha: there’s no way to set mix-blend-mode: difference on specific child elements, like headings, while avoiding the effect on images. I discovered there are several reasons why it doesn’t work, the first of which is that position: sticky cancels the blending.

The same goes when using something like transform: skewY on the container to add a little “tilt” to things. I suspect other properties don’t play well with the blending, but I didn’t go that far to find out which ones.

Here’s the demo without scrolling that removes the troubling properties:

Curtain call!

I enjoyed building this component, and I always love it when I can accomplish something using only HTML and CSS, especially when they work smoothly on every browser.

What will make with it? Is there a different way you would approach a “raise the curtain” effect like this? Let me know in the comments!


How to Make a “Raise the Curtains” Effect in CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/css-raise-the-curtains-effect/feed/ 2 363882
A Serene CSS Dappled Light Effect https://css-tricks.com/css-dappled-light-effect/ https://css-tricks.com/css-dappled-light-effect/#comments Wed, 19 Jan 2022 22:46:39 +0000 https://css-tricks.com/?p=360899 There’s a serene warmth to the early evening sunlight peaking through rustling leaves. Artists use dappled light to create a soft, hypnotic effect.

Bedford Dwellings by Ron Donoughe (2013)

We can create the same sort of dappled light effect in …


A Serene CSS Dappled Light Effect originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
There’s a serene warmth to the early evening sunlight peaking through rustling leaves. Artists use dappled light to create a soft, hypnotic effect.

An oil painting of a tall rectangular orange building with six windows, two by two, and a faint full-width fence in front of it. There is a similar building off in the distance. A tall birch tree is in the foreground with light green and yellow leaves, casting the dappled light effect that is being covered in this article. The shadows cover the green grass between the tree and building, and they extend to the building.
Bedford Dwellings by Ron Donoughe (2013)

We can create the same sort of dappled light effect in web design, using it on photos and illustrations to add that magic touch to what might otherwise be drab walls of content to bring them back to life.

I’ll give you one easy, quick way to add this effect… with just CSS.

Before we get into the code, it’s important to know the composition of dappled light. It’s made up of large spots — circular or elliptical — of light that are intercepted by the shadows cast by the foliage. Basically the light that slips past leaves, branches and so forth. Sometimes the shadows create crisp edges, but are more often blurred since we’re talking about light that passes though many, less defined spaces that diffuse and distort the light as it casts shadows from a further distance than, say, your own stark shadow on a nearby wall from direct sunlight.

Here’s the difference in the appearance of a white wall with and without lit by dappled light:

A side-by-side comparison of the same white brick surface, the left showing the CSS dappled light effect compared to no shadows.
The effect creates splashes of light and shadow.

I’m going to recreate the dappled light effect with both plain text and fun emojis, applying CSS shadows and blends to mimic nature. I’ll cover alternative methods too.

Setting the scene

We’ll use text — letters from the alphabet, special characters, emojis, etc. — to create the shapes of light. And by light, I mean pale, translucent colors. Again, we’re for a dappled light effect rather than something that’s sharp, crisp, or stark.

It’s best to choose characters that are elliptical or oblong in some way — the spots produced by dappled light comes in a variety of shapes. You’ll have to go with your best judgement here to get exactly what you’re going for. Me? I’m using 🍃, 🍂, \ because they are elliptical, oblong, and slanted — a bit of chaos and unpredictability for an otherwise serene effect.

I’m wrapping those in paragraphs that are contained in a .backdrop parent element:

<div class="backdrop">
  <p class="shapes">🍃</p>
  <p class="shapes">🍂</p>
  <p class="shapes">\</p>
</div>

I’m using the parent element as the surface where the dappled light and shadows are cast, applying a background image for its texture. And not only am I giving the surface an explicit width and height, but also setting hidden overflow on it so I’m able to cast shadows that go beyond the surface without revealing them. The objects that cast the dappled light effect are aligned in the middle of the backdrop’s surface, thanks to CSS grid:

.backdrop {
  background: center / cover no-repeat url('image.jpeg');
  width: 400px; height: 240px;
  overflow: hidden;
  display: grid;
}
.backdrop > * {
  grid-area: 1/1;
}

I find that it’s OK if the shapes aren’t aligned exactly on top of one another as long as they overlap in a way that gets the dappled light effect you want. So no pressure to do exactly what I’m doing here to position things in CSS. In fact, I encourage you to try playing with the values to get different patterns of dappled light!

Styling the dappled light in CSS

These are the key properties the emojis should have — transparent color, black semi-transparent background (using the alpha channel in rgba()), blurry white text-shadow with a nice large font-size, and finally, a mix-blend-mode to smooth things out.

.shapes {
  color:  transparent;
  background-color: rgba(0, 0, 0, 0.3); // Use alpha transparency
  text-shadow: 0 0 40px #fff; // Blurry white shadow
  font: bolder 320pt/320pt monospace;
  mix-blend-mode: multiply;
}

mix-blend-mode sets how an element’s colors blend with that of its container element’s content. The multiply value causes the backdrop of an element to show through the element’s light colors and keeps dark colors the same, making for a nicer and more natural dappled light effect.

Refining colors and contrast

I wanted the background-image on the backdrop to be a bit brighter, so I also added filter: brightness(1.6). Another way to do this is with background-blend-mode instead, where all the different backgrounds of an element are blended and, instead of adding the emojis as separate elements, we add them as background images.

Notice that I used a different emoji in that last example as well as floralwhite for some color that’s less intense than pure white for the light. Here’s one of the emoji background images unwrapped:

<svg xmlns='http://www.w3.org/2000/svg'> 
  <foreignObject width='400px' height='240px'> 
    <div xmlns='http://www.w3.org/1999/xhtml' style=
      'font: bolder 720pt/220pt monospace;
       color: transparent;
       text-shadow: 0 0 40px floralwhite;
       background: rgba(0, 0, 0, 0.3);'
    >
      🌾
    </div> 
  </foreignObject> 
</svg>

If you want to use your own images for the shapes, ensure the borders are blurred to create a soft light. The CSS blur() filter can be handy for the same sort of thing. I also used CSS @supports to adjust the shadow blur value for certain browsers as a fallback.

Now let’s circle back to the first example and add a few things:

<div class="backdrop">
  <p class="shapes">🍃</p>
  <p class="shapes">🍂</p>
  <p class="shapes">\</p>
</div>

<p class="content">
  <img width="70px" style="float: left; margin-right: 10px;" src="image.jpeg" alt="">
  Top ten tourists spots for the summer vacation <br><br><i style="font-weight: normal;">Here are the most popular places...</i>
</p>

.backdrop and .shapes are basically the same styles as before. As for the .content, which also sits on top of the .backdrop, I added isolation: isolate to form a new stacking context, excluding the element from the blending as a refining touch.

Animating the light source

I also decided to add a simple CSS animation with @keyframes that get applied to the .backdrop on :hover:

.backdrop:hover > .shapes:nth-of-type(1){
  animation: 2s ease-in-out infinite alternate move;
}
.backdrop:hover > .shapes:nth-of-type(2):hover{
  animation: 4s ease-in-out infinite alternate move-1;
}

@keyframes move {
  from {
    text-indent: -20px;
  }
  to {
    text-indent: 20px;
  }
}
@keyframes move-1 {
  from {
    text-indent: -60px;
  }
  to {
    text-indent: 40px;
  }
}

Animating the text-indent property on the emojis products a super subtle bit of movement — the kind you might expect from clouds moving overhead that change the direction of the light. Just a touch of class, you know.

Wrapping up

There we have it! We drew some inspiration from nature and art to mimic one of those partly cloudy days where the sun shines through trees and bushes, projecting dappled light and shadow spots against a surface. And we did all of it with a small handful of CSS and a few emoji.

The key was how we applied color on the emoji. Using an extra blurry text-shadow in a light color sets the light, and a semi-transparent background-color defines the shadow spots. From there, all we had to do was ensure the backdrop for the light and shadows used a realistic texture with enough contrast to see the dappled light effect in action.


A Serene CSS Dappled Light Effect originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/css-dappled-light-effect/feed/ 8 360899
Fractional SVG stars with CSS https://css-tricks.com/fractional-svg-stars-with-css/ https://css-tricks.com/fractional-svg-stars-with-css/#respond Thu, 02 Dec 2021 15:42:37 +0000 https://css-tricks.com/?p=358210 Some ⭐️⭐️⭐️⭐️⭐️ star rating systems aren’t always exactly even stars. Say you want to support rating something 2.25 stars. To do that you could “fill” the shape of the stars partially. I like this idea by Samuel Kraft. The tricky …


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

]]>
Some ⭐️⭐️⭐️⭐️⭐️ star rating systems aren’t always exactly even stars. Say you want to support rating something 2.25 stars. To do that you could “fill” the shape of the stars partially. I like this idea by Samuel Kraft. The tricky part is:

The final step is making the overlay div only affect the star SVGs beneath, not the background. We can do this by using the CSS mix-blend-mode property with the color value.

Check out Samuel’s post for an interactive demo and deeper information, but I thought I’d give it a crack myself to get a feel for the idea:

The idea is that this is an overlay on top of the stars. You can’t see it and it doesn’t affect the stars because it’s either black or white and mix-blend-mode: color; means that overlay will only effect elements that do have color.

There are loads of ways to do rating stars, for the record. We covered five of them a little while back. One rather clever method in there is using unicode stars (like, as text), then filling their background with -webkit-background-clip: text; which means you can partially fill them (like with a hard-stop linear-gradient()). Solid trickery, that.

To Shared LinkPermalink on CSS-Tricks


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

]]>
https://css-tricks.com/fractional-svg-stars-with-css/feed/ 0 358210
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
My Struggle to Use and Animate a Conic Gradient in SVG https://css-tricks.com/my-struggle-to-use-and-animate-a-conic-gradient-in-svg/ https://css-tricks.com/my-struggle-to-use-and-animate-a-conic-gradient-in-svg/#comments Thu, 08 Jul 2021 14:35:38 +0000 https://css-tricks.com/?p=343623 The wonderful company I work for, Payoneer, has a new logo, and my job was to recreate it and animate it for a loader component in our app. I’ll explain exactly how I did it, share the problems I …


My Struggle to Use and Animate a Conic Gradient in SVG originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
The wonderful company I work for, Payoneer, has a new logo, and my job was to recreate it and animate it for a loader component in our app. I’ll explain exactly how I did it, share the problems I had, and walk you through the solution I came up with. And, as a bonus, we’ll look at animating it!

But first, I guess some of you are asking yourselves… Recreate it? Why?

The branding agency that designed our logo sent us a full set of assets categorized by themes. They came in all sizes and in every available format. We had everything, including SVGs, for the logo and the loader animation. But we couldn’t use them.

Here’s why. Let’s take a look at the logo:

We call the new logo “The Halo.”

The logo is a ring with a conic gradient that consists of five colors, and… that’s it. The problem is that SVG doesn’t support angled gradients (for now, at least), so when we export a design that has a conic gradient as an SVG, we need some sort of hack to get the desired result.

Now, I’m no expert when it comes to working with vector graphic software, so there might be a different (and perhaps better) way to do this, but I know that the most common way to export conic gradients to SVG is to convert the gradient element to an image and insert that image into the SVG as a base64 string. That’s also what we got from the branding agency, and I trust them to know the best way to export an SVG.

But, since the final SVG file now contains a PNG base64 string, the file size jumped to nearly 1MB, which might not be a total disaster, but it’s much higher than the 2KB that it should be. Multiply that difference by three themes (no text, light text, and dark text variations), and we’re looking at 3MB worth of images instead of 3KB worth of code. That’s a big difference, so we’ve decided to recreate the logo with SVG.

But how?!

Even though CSS fully supports conic gradients, SVG does not. So the first question I asked myself was how to create a conic gradient in SVG. Actually, I asked Google. And what I found was a lot of cool, unique, creative ways to add a conic gradients to SVG, most of them relying on some sort of clip-path implementation. I first created a short <path> that represents the shape of the ring and used it as a clip-path on a simple <rect> element.

Next, I needed to fill the <rect> with conic gradients, but first, I had to find all the correct color stops to recreate the look. That took a while, but after a lot of fine tuning, I got a result I’m happy with:

div.gradient {
  background-image: conic-gradient(from 270deg, #ff4800 10%, #dfd902 35%, #20dc68, #0092f4, #da54d8 72% 75%, #ff4800 95%);
}

The last step was to replace the <rect> with something else that supports conic gradients, and the simplest way I’ve found is to use an SVG <foreignObject> element with a regular <div> inside it, and a conic-gradient as a background-image. Then all I needed to do was to set the clip-path on the <foreignObject> element, and that’s it.

So, that’s how I used a conic gradient in an SVG to keep the design fully vector and scalable with less than 20 lines of code, and less than 2KB in file size.

But that was the easy part. Now let’s talk animation.

The loader

Our app shows a loading animation every time a user logs in. We had been using a GIF file for it, but I had been meaning to update it to a pure CSS/SVG animation for months. The benefits are obvious: faster render means a more seamless loading experience, and a smaller file size means even faster loading. We simply get more for less, which is especially ideal for a loading animation.

Here’s the animation I was aiming for:

This type of animation is actually fairly easy with SVG. All we really need is a trick using stroke-dasharray and stroke-dashoffset. That was my starting point. I created a new <path> in the center of the ring, removed the fill, added a stroke with the right stroke-width, and then worked on the animation.

It took me some playing around to get the movement just like the designers wanted it. I ended up using two animations, actually: one controls the stroke-dashoffset, and the second rotates the entire <path> a full turn.

But, since the clip-path property refers to the fill of the shape, animating the stroke meant I had to solve one of two problems: I could either find a different way to animate the movement, or find a different way to add the colors to the stroke.

So I went back to Google and all of the creative ideas I found before, but most of them were pretty much un-animatable, so I started looking for a good non-clip-path way to add colors to the stroke. I looked at a few “out-of-the-box” solutions, checked out masking, and ended up with the simplest perfect solution:

.logoBlend {
  mix-blend-mode: lighten;
}

A lighten blend mode looks at the RGB colors of each pixel of the rendered element, compares it to the RGB value of the background pixel that’s behind it, and keeps whichever is highest. That means that the parts of the element that are white will remain white, and the dark parts will get the values of the background pixel.

By adding a white <rect> to the black path, I essentially blocked anything that’s behind it. Meanwhile, everything that’s behind the animated black stroke is visible. That meant I could bring back the <foreignObject> with the conic-gradient, put it behind the mix-blend-mode layer, and give it a simple rotate animation to match the design.

Note that the end result of this method will have a white background, not transparent like the static logo, but I was fine with that. If you need to, you can flip it around, use black background, and hide the light parts of your element by setting the blend mode to darken.

Final touches

I was pretty much done at this point, and quite happy with the end result. But a couple of days later, I got a Lottie-based JSON file from the branding agency with the exact same animation. In retrospect, maybe I could spare my work and use their file, it would have worked just fine. Even the file size was surprisingly small, but it was still 8✕ bigger than the SVG, so we ended up using my animation anyway.

But, that meant I had one last thing to do. The Lottie animation had a “start animation” where the small orange dot grows into view, and I had to add it to my animation as well. I added a short 0.5s delay to all three animations as well as a scaling animation in the beginning.

Click on “Rerun” on the Pen to see the animation again from the initial dot.

That’s it! Now my company has a new logo and a set of lightweight, fully scalable assets to use across our web platforms.

And for those of you wondering, yes, I did end up creating a nice little Logo component in React since we’re using it. It even renders the SVG according to a theme passed to it as a prop, making the implementation easier, and keeping all future changes in a single location.

What about you?

Do you think there’s a better way to get the same result? Share your thoughts in the comments! And thank you for reading.


My Struggle to Use and Animate a Conic Gradient in SVG originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/my-struggle-to-use-and-animate-a-conic-gradient-in-svg/feed/ 14 343623
Taming Blend Modes: `difference` and `exclusion` https://css-tricks.com/taming-blend-modes-difference-and-exclusion/ https://css-tricks.com/taming-blend-modes-difference-and-exclusion/#comments Mon, 22 Mar 2021 14:48:35 +0000 https://css-tricks.com/?p=334463 Up until 2020, blend modes were a feature I hadn’t used much because I rarely ever had any idea what result they could produce without giving them a try first. And taking the “try it and see what happens” approach …


Taming Blend Modes: `difference` and `exclusion` originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Up until 2020, blend modes were a feature I hadn’t used much because I rarely ever had any idea what result they could produce without giving them a try first. And taking the “try it and see what happens” approach seemed to always leave me horrified by the visual vomit I had managed to create on the screen.

The problem stemmed from not really knowing how they work in the back. Pretty much every article I’ve seen on the topic is based on examples, comparisons with Photoshop or verbose artistic descriptions. I find examples great, but when you have no clue how things work in the back, adapting a nice-looking demo into something that would implement a different idea you have in your head becomes a really time-consuming, frustrating and ultimately futile adventure. Then Photoshop comparisons are pretty much useless for someone coming from a technical background. And verbose artistic descriptions feel like penguin language to me.

So I had a lightbulb moment when I came across the spec and found it also includes mathematical formulas according to which blend modes work. This meant I could finally understand how this stuff works in the back and where it can be really useful. And now that I know better, I’ll be sharing this knowledge in a series of articles.

Today, we’ll focus on how blending generally works, then take a closer look at two somewhat similar blend modes — difference and exclusion — and, finally, get to the meat of this article where we’ll dissect some cool use cases like the ones below.

A few examples of what we can achieve with these two blend modes.

Let’s discuss the “how” of blend modes

Blending means combining two layers (that are stacked one on top of the other) and getting a single layer. These two layers could be two siblings, in which case the CSS property we use is mix-blend-mode. They could also be two background layers, in which case the CSS property we use is background-blend-mode. Note that when I talk about blending “siblings,” this includes blending an element with the pseudo-elements or with the text content or the background of its parent. And when it comes to background layers, it’s not just the background-image layers I’m talking about — the background-color is a layer as well.

When blending two layers, the layer on top is called the source, while the layer underneath is called the destination. This is something I just take as it is because these names don’t make much sense, at least to me. I’d expect the destination to be an output, but instead they’re both inputs and the resulting layer is the output.

Illustration showing two layers. The top layer is the source, while the bottom one is the destination.
Blending terminology

How exactly we combine the two layers depends on the particular blend mode used, but it’s always per pixel. For example, the illustration below uses the multiply blend mode to combine the two layers, represented as grids of pixels.

Illustration showing two corresponding pixels of the two layers being blended, which results in the corresponding pixel of the resulting layer.
How blending two layers works at a pixel level

Alright, but what happens if we have more than two layers? Well, in this case, the blending process happens in stages, starting from the bottom.

In a first stage, the second layer from the bottom is our source, and the first layer from the bottom is our destination. These two layers blend together and the result becomes the destination for the second stage, where the third layer from the bottom is the source. Blending the third layer with the result of blending the first two gives us the destination for the third stage, where the fourth layer from the bottom is the source.

Illustration showing the process described above.
Blending multiple layers

Of course, we can use a different blend mode at each stage. For example, we can use difference to blend the first two layers from the bottom, then use multiply to blend the result with the third layer from the bottom. But this is something we’ll go a bit more into in future articles.

The result produced by the two blend modes we discuss here doesn’t depend on which of the two layers is on top. Note that this is not the case for all possible blend modes, but it is the case for the ones we’re looking at in this article.

They are also separable blend modes, meaning the blending operation is performed on each channel separately. Again, this is not the case for all possible blend modes, but it is the case for difference and exclusion.

More exactly, the resulting red channel only depends on the red channel of the source and the red channel of the destination; the resulting green channel only depends on the green channel of the source and the green channel of the destination; and finally, the resulting blue channel only depends on the blue channel of the source and the blue channel of the destination.

R = fB(Rs, Rd)
G = fB(Gs, Gd)
B = fB(Bs, Bd)

For a generic channel, without specifying whether it’s red, green or blue, we have that it’s a function of the two corresponding channels in the source (top) layer and in the destination (bottom) layer:

Ch = fB(Chs, Chd)

Something to keep in mind is that RGB values can be represented either in the [0, 255] interval, or as percentages in the [0%, 100%] interval, and what we actually use in our formulas is the percentage expressed as a decimal value. For example, crimson can be written as either rgb(220, 20, 60) or as rgb(86.3%, 7.8%, 23.5%) — both are valid. The channel values we use for computations if a pixel is crimson are the percentages expressed as decimal values, that is .863, .078, .235.

If a pixel is black, the channel values we use for computations are all 0, since black can be written as rgb(0, 0, 0) or as rgb(0%, 0%, 0%). If a pixel is white, the channel values we use for computations are all 1, since white can be written as rgb(255, 255, 255) or as rgb(100%, 100%, 100%).

Note that wherever we have full transparency (an alpha equal to 0), the result is identical to the other layer.

difference

The name of this blend mode might provide a clue about what the blending function fB() does. The result is the absolute value of the difference between the corresponding channel values for the two layers.

Ch = fB(Chs, Chd) = |Chs - Chd|

First off, this means that if the corresponding pixels in the two layers have identical RGB values (i.e. Chs = Chd for every one of the three channels), then the resulting layer’s pixel is black since the differences for all three channels are 0.

Chs = Chd
Ch = fB(Chs, Chd) = |Chs - Chd| = 0

Secondly, since the absolute value of the difference between any positive number and 0 leaves that number unchanged, it results in the corresponding result pixel having the same RGB value as the other layer’s pixel if a layer’s pixel is black (all channels equal 0).

If the black pixel is in the top (source) layer, replacing its channel values with 0 in our formula gives us:

Ch = fB(0, Chd) = |0 - Chd| = |-Chd| = Chd

If the black pixel is in the bottom (destination) layer, replacing its channel values with 0 in our formula gives us:

Ch = fB(Chs, 0) = |Chs - 0| = |Chs| = Chs

Finally, since the absolute value of the difference between any positive subunitary number and 1 gives us the complement of that number, it results that if a layer’s pixel is white (has all channels 1), the corresponding result pixel is the other layer’s pixel fully inverted (what filter: invert(1) would do to it).

If the white pixel is in the top (source) layer, replacing its channel values with 1 in our formula gives us:

Ch = fB(1, Chd) = |1 - Chd| = 1 - Chd

If the white pixel is in the bottom (destination) layer, replacing its channel values with 1 in our formula gives us:

Ch = fB(Chs, 1) = |Chs - 1| = 1 - Chs

This can be seen in action in the interactive Pen below, where you can toggle between viewing the layers separated and viewing them overlapping and blended. Hovering the three columns in the overlapping case also reveals what’s happening for each.

exclusion

For the second and last blend mode we’re looking at today, the result is twice the product of the two channel values, subtracted from their sum:

Ch = fB(Chs, Chd) = Chs + Chd - 2·Chs·Chd

Since both values are in the [0, 1] interval, their product is always at most equal to the smallest of them, so twice the product is always at most equal to their sum.

If we consider a black pixel in the top (source) layer, then replace Chs with 0 in the formula above, we get the following result for the corresponding result pixel’s channels:

Ch = fB(0, Chd) = 0 + Chd - 2·0·Chd = Chd - 0 = Chd

If we consider a black pixel in the bottom (destination) layer, then replace Chd with 0 in the formula above, we get the following result for the corresponding result pixel’s channels:

Ch = fB(Chs, 0) = Chs + 0 - 2·Chs·0 = Chs - 0 = Chs

So, if a layer’s pixel is black, it results that the corresponding result pixel is identical to the other layer’s pixel.

If we consider a white pixel in the top (source) layer, then replace Chs with 1 in the formula above, we get the following result for the corresponding result pixel’s channels:

Ch = fB(1, Chd) = 1 + Chd - 2·1·Chd = 1 + Chd - 2·Chd = 1 - Chd

If we consider a white pixel in the bottom (destination) layer, then replace Chd with 1 in the formula above, we get the following result for the corresponding result pixel’s channels:

Ch = fB(Chs, 1) = Chs + 1 - 2·Chs·1 = Chs + 1 - 2·Chs = 1 - Chs

So if a layer’s pixel is white, it results that the corresponding result pixel is identical to the other layer’s pixel inverted.

This is all shown in the following interactive demo:

Note that as long as at least one of the layers only has black and white pixels, difference and exclusion produce the exact same result.

Now, let’s turn to the “what” of blend modes

Here comes the interesting part — the examples!

Text state change effect

Let’s say we have a paragraph with a link:

<p>Hello, <a href='#'>World</a>!</div>

We start by setting a few basic styles to put our text in the middle of the screen, bump up its font-size, set a background on the body and a color on both the paragraph and the link.

body {
  display: grid;
  place-content: center;
  height: 100vh;
  background: #222;
  color: #ddd;
  font-size: clamp(1.25em, 15vw, 7em);
}

a { color: gold; }

Doesn’t look like much so far, but we’ll soon change that!

Screenshot of the result after setting the initial styles. The paragraph text is in the middle. Its normal text is while, while the link text is gold.
What we have so far (demo)

The next step is to create an absolutely positioned pseudo-element that covers the entire link and has its background set to currentColor.

a {
  position: relative;
  color: gold;
  
  &::after {
    position: absolute;
    top: 0;
    bottom: 0;
    right: 0;
    left: 0;
    background: currentColor;
    content: '';
  }
}
Screenshot of the result after creating and setting a few basic styles on the link pseudo: this now covers the entire link text.
The result we now have with the pseudo-element on the link (demo)

The above looks like we’ve messed things up… but have we really? What we have here is a gold rectangle on top of gold text. And if you’ve paid attention to how the two blend modes discussed above work, then you’ve probably already guessed what’s next — we blend the two sibling nodes within the link (the pseudo-element rectangle and the text content) using difference, and since they’re both gold, it results that what they have in common — the text — becomes black.

p { isolation: isolate; }

a {
  /* same as before */
  
  &::after {
    /* same as before */
    mix-blend-mode: difference;
  }
}

Note that we have to isolate the paragraph to prevent blending with the body background. While this is only an issue in Firefox (and given we have a very dark background on the body, it’s not too noticeable) and is fine in Chrome, keep in mind that, according to the spec, what Firefox does is actually correct. It’s Chrome that’s behaving in a buggy way here, so we should have the isolation property set in case the bug gets fixed.

Screenshot of the result after blending the link pseudo with the link text. Since they're both gold, the result is black text on gold background.
The mix-blend-mode: difference effect (demo)

Alright, but we want this to happen only if the link is focused or hovered. Otherwise, the pseudo-element isn’t visible — let’s say it’s scaled down to nothing.

a {
  /* same as before */
  text-decoration: none;
  
  &::after {
    /* same as before */
    transform: scale(0);
  }

  &:focus { outline: none }
  &:focus, &:hover { &::after { transform: none; } }
}

We’ve also removed the link underline and the focus outline. Below, you can now see the difference effect on :hover (the same effect occurs on :focus, which is something you can test in the live demo).

Animated gif. On hover, the gold pseudo rectangle suddenly shows up and is blended with the gold link text using the difference blend mode, thus making the latter black.
The mix-blend-mode: difference effect only on :hover (demo)

We now have our state change, but it looks rough, so let’s add a transition!

a {
  /* same as before */
  
  &::after {
    /* same as before */
    transition: transform .25s;
  }
}

Much better!

Animated gif. On hover, the gold pseudo rectangle smoothly grows from nothing and is blended with the gold link text using the difference blend mode, thus making their intersection black.
The mix-blend-mode: difference effect only on :hover, now smoothed by a transition (demo)

It would look even better if our pseudo grew not from nothing in the middle, but from a thin line at the bottom. This means we need to set the transform-origin on the bottom edge (at 100% vertically and whatever value horizontally) and initially scale our pseudo to something slightly more than nothing along the y axis.

a {
  /* same as before */
  
  &::after {
    /* same as before */
    transform-origin: 0 100%;
    transform: scaleY(.05);
  }
}
Animated gif. On hover, the gold pseudo rectangle smoothly grows from a thin underline to a rectangle covering its link parent's bounding box and is blended with the gold link text using the difference blend mode, thus making their intersection black.
The mix-blend-mode: difference effect only on :hover, now smoothed by a transition between a thin underline and a rectangle containing the link text (demo)

Something else I’d like to do here is replace the font of the paragraph with a more aesthetically appealing one, so let’s take care of that too! But we now have a different kind of problem: the end of the ‘d’ sticks out of the rectangle on :focus/:hover.

Screenshot illustrating the problem with slanted text — the last letter ends outside of the pseudo-element rectangle.
The problem illustrated: the end of the “d” sticks out when we :focus or :hover the link (demo)

We can fix this with a horizontal padding on our link.

a {
  /* same as before */
  padding: 0 .25em;
}

In case you’re wondering why we’re setting this padding on both the right and the left side instead of just setting a padding-right, the reason is illustrated below. When our link text becomes “Alien World,” the curly start of the ‘A’ would end up outside of our rectangle if we didn’t have a padding-left.

Screenshot illustrating the problem with only setting a lateral padding in the direction on the slant (right in this case): if our link text becomes 'Alien World', the curly start of the 'A' falls outside the pseudo-element rectangle. This is solved by having a lateral padding on both sides.
Why we have padding on both lateral sides (demo)

This demo with a multi-word link above also highlights another issue when we reduce the viewport width.

Animated gif. Shows how in the case of a multi-line link, the pseudo-element is just between the left of the first word in the link text and the last word in the same link text.
The problem with multi-line links (demo)

One quick fix here would be to set display: inline-block on the link. This isn’t a perfect solution. It also breaks when the link text is longer than the viewport width, but it works in this particular case, so let’s just leave it here now and we’ll come back to this problem in a little while.

Animated gif. Shows the inline-block fix in action in this particular case.
The inline-block solution (demo)

Let’s now consider the situation of a light theme. Since there’s no way to get white instead of black for the link text on :hover or :focus by blending two identical highlight layers that are both not white, we need a bit of a different approach here, one that doesn’t involve using just blend modes.

What we do in this case is first set the background, the normal paragraph text color, and the link text color to the values we want, but inverted. I was initially doing this inversion manually, but then I got the suggestion of using the Sass invert() function, which is a very cool idea that really simplifies things. Then, after we have this dark theme that’s basically the light theme we want inverted, we get our desired result by inverting everything again with the help of the CSS invert() filter function.

Tiny caveat here: we cannot set filter: invert(1) on the body or html elements because this is not going to behave the way we expect it to and we won’t be getting the desired result. But we can set both the background and the filter on a wrapper around our paragraph.

<section>
  <p>Hello, <a href='#'>Alien World</a>!</p>
</section>
body {
  /* same as before, 
     without the place-content, background and color declarations, 
     which we move on the section */
}

section {
  display: grid;
  place-content: center;
  background: invert(#ddd) /* Sass invert(<color>) function */;
  color: invert(#222); /* Sass invert<color>) function */;
  filter: invert(1); /* CSS filter invert(<number|percentage>) function */
}

a {
  /* same as before */
  color: invert(purple); /* Sass invert(<color>) function */
}

Here’s an example of a navigation bar employing this effect (and a bunch of other clever tricks, but those are outside the scope of this article). Select a different option to see it in action:

Something else we need to be careful with is the following: all descendants of our section get inverted when we use this technique. And this is probably not what we want in the case of img elements — I certainly don’t expect to see the images in a blog post inverted when I switch from the dark to the light theme. Consequently, we should reverse the filter inversion on every img descendant of our section.

section {
  /* same as before */
  
  &, & img { filter: invert(1); }
}

Putting it all together, the demo below shows both the dark and light theme cases with images:

Now let’s get back to the wrapping link text issue and see if we don’t have better options than making the a elements inline-block ones.

Well, we do! We can blend two background layers instead of blending the text content and a pseudo. One layer gets clipped to the text, while the other one is clipped to the border-box and its vertical size animates between 5% initially and 100% in the hovered and focused cases.

a {
  /* same as before */
  -webkit-text-fill-color: transparent;
     -moz-text-fill-color: transparent;
  --full: linear-gradient(currentColor, currentColor);
  background: 
    var(--full), 
    var(--full) 0 100%/1% var(--sy, 5%) repeat-x;
  -webkit-background-clip: text, border-box;
          background-clip: text, border-box;
  background-blend-mode: difference;
  transition: background-size .25s;
	
  &:focus, &:hover { --sy: 100%; }
}

Note that we don’t even have a pseudo-element anymore, so we’ve taken some of the CSS on it, moved it on the link itself, and tweaked it to suit this new technique. We’ve switched from using mix-blend-mode to using background-blend-mode; we’re now transitioning background-size of transform and, in the :focus and :hover states; and we’re now changing not the transform, but a custom property representing the vertical component of the background-size.

Animated gif. Shows the result when we blend two background layers on the actual link: one clipped to text and the other one clipped to border-box.
The background layer blending solution (demo).

Much better, though this isn’t a perfect solution either.

The first problem is one you’ve surely noticed if you checked the caption’s live demo link in Firefox: it doesn’t work at all. This is due to a Firefox bug I apparently reported back in 2018, then forgot all about until I started toying with blend modes and hit it again.

The second problem is one that’s noticeable in the recording. The links seem somewhat faded. This is because, for some reason, Chrome blends inline elements like links (note that this won’t happen with block elements like divs) with the background of their nearest ancestor (the section in this case) if these inline elements have background-blend-mode set to anything but normal.

Even more weirdly, setting isolation: isolate on the link or its parent paragraph doesn’t stop this from happening. I still had a nagging feeling it must have something to do with context, so I decided to keep throwing possible hacks at it, and hope maybe something ends up working. Well, I didn’t have to spend much time on it. Setting opacity to a subunitary (but still close enough to 1 so it’s not noticeable that it’s not fully opaque) value fixes it.

a {
  /* same as before */
  opacity: .999; /* hack to fix blending issue ¯_(ツ)_/¯ */
}
Animated gif. Shows the result after applying the opacity hackaround.
Result after fixing the blending issue (demo)

The final problem is another one that’s noticeable in the recording. If you look at the ‘r’ at the end of “Amur” you can notice its right end is cut out as it falls outside the background rectangle. This is particularly noticeable if you compare it with the ‘r’ in “leopard.”

I didn’t have high hopes for fixing this one, but threw the question to Twitter anyway. And what do you know, it can be fixed! Using box-decoration-break in combination with the padding we have already set can help us achieve the desired effect!

a {
  /* same as before */
  box-decoration-break: clone;
}

Note that box-decoration-break still needs the -webkit- prefix for all WebKit browsers, but unlike in the case of properties like background-clip where at least one value is text, auto-prefixing tools can take care of the problem just fine. That’s why I haven’t included the prefixed version in the code above.

Animated gif. Shows the result after applying the box-decoration-break solution.
Result after fixing the text clipping issue (demo).

Another suggestion I got was to add a negative margin to compensate for the padding. I’m going back and forth on this one — I can’t decide whether I like the result better with or without it. In any event, it’s an option worth mentioning.

$p: .25em;

a {
  /* same as before */
  margin: 0 (-$p); /* we put it within parenthesis so Sass doesn't try to perform subtraction */
  padding: 0 $p;
}
Animated gif. Shows the result after setting a negative margin to compensate for the padding.
Result when we have a negative margin compensating for the padding (demo)

Still, I have to admit that animating just the background-position or the background-size of a gradient is a bit boring. But thanks to Houdini, we can now get creative and animate whatever component of a gradient we wish, even though this is only supported in Chromium at the moment. For example, the radius of a radial-gradient() like below or the progress of a conic-gradient().

Animated gif. Shows a random bubble growing from nothing and being blended with the text of a navigation link every time this is being hovered or focused.
Bubble effect navigation (demo)

Invert just an area of an element (or a background)

This is the sort of effect I often see achieved by either using element duplication — the two copies are layered one on top of the other, where one of them has an invert filter and clip-path is used on the top one in order to show both of layers. Another route is layering a second element with an alpha low enough you cannot even tell it’s there and a backdrop-filter.

Both these approaches get the job done if we want to invert a part of the entire element with all its content and descendants, but they cannot help us when we want to invert just a part of the background — both filter and backdrop-filter affect entire elements, not just their backgrounds. And while the new filter() function (already supported by Safari) does have effect solely on background layers, it affects the entire area of the background, not just a part of it.

This is where blending comes in. The technique is pretty straightforward: we have a background layer, part of which we want to invert and one or more gradient layers that give us a white area where we want inversion of the other layer and transparency (or black) otherwise. Then we blend using one of the two blend modes discussed today. For the purpose of inversion, I prefer exclusion (it’s one character shorter than difference).

Here’s a first example. We have a square element that has a two-layer background. The two layers are a picture of a cat and a gradient with a sharp transition between white and transparent.

div {
  background: 
    linear-gradient(45deg, white 50%, transparent 0), 
    url(cat.jpg) 50%/ cover;
}

This gives us the following result. We’ve also set dimensions, a border-radius, shadows, and prettified the text in the process, but all that stuff isn’t really important in this context:

Screenshot. Shows a square where the photo of a cat is covered in the lower left half (below the main diagonal) by a solid white background.
The two backgrounds layered

Next, we just need one more CSS declaration to invert the lower left half:

div {
  /* same as before */
  background-blend-mode: exclusion; /* or difference, but it's 1 char longer */
}

Note how the text is not affected by inversion; it’s only applied to the background.

Screenshot. Shows a square with a cat background, where the lower left half (below the main diagonal) has been inverted (shows the image negative).
Final result (demo)

You probably know the interactive before-and-after image sliders. You may have even seen something of the kind right here on CSS-Tricks. I’ve seen it on Compressor.io, which I often use to compress images, including the ones used in these articles!

Our goal is to create something of the kind using a single HTML element, under 100 bytes of JavaScript — and not even much CSS!

Our element is going to be a range input. We don’t set its min or max attributes, so they default to 0 and 100, respectively. We don’t set the value attribute either, so it defaults to 50, which is also the value we give a custom property, --k, set in its style attribute.

<input type='range' style='--k: 50'/>

In the CSS, we start with a basic reset, then we make our input a block element that occupies the entire viewport height. We also give dimensions and dummy backgrounds to its track and thumb just so that we can start seeing stuff on the screen right away.

$thumb-w: 5em;

@mixin track() {
  border: none;
  width: 100%;
  height: 100%;
  background: url(flowers.jpg) 50%/ cover;
}

@mixin thumb() {
  border: none;
  width: $thumb-w;
  height: 100%;
  background: purple;
}

* {
  margin: 0;
  padding: 0;
}

[type='range'] {
  &, &::-webkit-slider-thumb, 
  &::-webkit-slider-runnable-track { -webkit-appearance: none; }
  
  display: block;
  width: 100vw; height: 100vh;
  
  &::-webkit-slider-runnable-track { @include track; }
  &::-moz-range-track { @include track; }
  
  &::-webkit-slider-thumb { @include thumb; }
  &::-moz-range-thumb { @include thumb; }
}
Screenshot. Shows a tall slider with an image background and a tall narrow purple thumb.
What we have so far (demo)

The next step is to add another background layer on the track, a linear-gradient one where the separation line between transparent and white depends on the current range input value, --k, and then blend the two.

@mixin track() {
  /* same as before */
  background:
    url(flowers.jpg) 50%/ cover, 
    linear-gradient(90deg, transparent var(--p), white 0);
  background-blend-mode: exclusion;
}

[type='range'] {
  /* same as before */
  --p: calc(var(--k) * 1%);
}

Note that the order of the two background layers of the track doesn’t matter as both exclusion and difference are commutative.

It’s starting to look like something, but dragging the thumb does nothing to move the separation line. This is happening because the current value, --k (on which the gradient’s separation line position, --p, depends), doesn’t get automatically updated. Let’s fix that with a tiny bit of JavaScript that gets the slider value whenever it changes then sets --k to this value.

addEventListener('input', e => {
  let _t = e.target;
  _t.style.setProperty('--k', +_t.value)
})

Now all seems to be working fine!

But is it really? Let’s say we do something a bit fancier for the thumb background:

$thumb-r: .5*$thumb-w;
$thumb-l: 2px;

@mixin thumb() {
  /* same as before */
  --list: #fff 0% 60deg, transparent 0%;
  background: 
    conic-gradient(from 60deg, var(--list)) 0/ 37.5% /* left arrow */, 
    conic-gradient(from 240deg, var(--list)) 100%/ 37.5% /* right arrow */, 
    radial-gradient(circle, 
      transparent calc(#{$thumb-r} - #{$thumb-l} - 1px) /* inside circle */, 
      #fff calc(#{$thumb-r} - #{$thumb-l}) calc(#{$thumb-r} - 1px) /* circle line */, 
      transparent $thumb-r /* outside circle */), 
    linear-gradient(
      #fff calc(50% - #{$thumb-r} + .5*#{$thumb-l}) /* top line */, 
      transparent 0 calc(50% + #{$thumb-r} - .5*#{$thumb-l}) /* gap behind circle */, 
      #fff 0 /* bottom line */) 50% 0/ #{$thumb-l};
  background-repeat: no-repeat;
}

The linear-gradient() creates the thin vertical separation line, the radial-gradient() creates the circle, and the two conic-gradient() layers create the arrows.

The problem is now obvious when dragging the thumb from one end to the other: the separation line doesn’t remain fixed to the thumb’s vertical midline.

When we set --p to calc(var(--k)*1%), the separation line moves from 0% to 100%. It should really be moving from a starting point that’s half a thumb width, $thumb-r, until half a thumb width before 100%. That is, within a range that’s 100% minus a thumb width, $thumb-w. We subtract a half from each end, so that’s a whole thumb width to be subtracted. Let’s fix that!

--p: calc(#{$thumb-r} + var(--k) * (100% - #{$thumb-w}) / 100);

Much better!

But the way range inputs work, their border-box moving within the limits of the track’s content-box (Chrome) or within the limits of the actual input’s content-box (Firefox)… this still doesn’t feel right. It would look way better if the thumb’s midline (and, consequently, the separation line) went all the way to the viewport edges.

We cannot change how range inputs work, but we can make the input extend outside the viewport by half a thumb width to the left and by another half a thumb width to the right. This makes its width equal to that of the viewport, 100vw, plus an entire thumb width, $thumb-w.

body { overflow: hidden; }

[type='range'] {
  /* same as before */
  margin-left: -$thumb-r;
  width: calc(100vw + #{$thumb-w});
}

A few more prettifying tweaks related to the cursor and that’s it!

A fancier version of this (inspired by the Compressor.io website) is to put the input within a card whose 3D rotation also changes when the mouse moves over it.

We could also use a vertical slider. This is slightly more complex as our only reliable cross-browser way of creating custom styled vertical sliders is to apply a rotation on them, but this would also rotate the background. What we do is set the --p value and these backgrounds on the (not rotated) slider container, then keep the input and its track completely transparent.

This can be seen in action in the demo below, where I’m inverting a photo of me showing off my beloved Kreator hoodie.

We may of course use a radial-gradient() for a cool effect too:

background: 
  radial-gradient(circle at var(--x, 50%) var(--y, 50%), 
    #000 calc(var(--card-r) - 1px), #fff var(--card-r)) border-box, 
  $img 50%/ cover;

In this case, the position given by the --x and --y custom properties is computed from the mouse motion over the card.

The inverted area of the background doesn’t necessarily have to be created by a gradient. It can also be the area behind a heading’s text, as shown in this older article about contrasting text against a background image.

Gradual inversion

The blending technique for inversion is more powerful than using filters in more than one way. It also allows us to apply the effect gradually along a gradient. For example, the left side is not inverted at all, but then we progress to the right all the way to full inversion.

In order to understand how to get this effect, we must first understand how to get the invert(p) effect, where p can be any value in the [0%, 100%] interval (or in the [0, 1] interval if we use the decimal representation).

The first method, which works for both difference and exclusion is setting the alpha channel of our white to p. This can be seen in action in the demo below, where dragging the slider controls the invrsion progress:

In case you’re wondering about the hsl(0, 0%, 100% / 100%) notation, this is now a valid way of representing a white with an alpha of 1, according the spec.

Furthermore, due to the way filter: invert(p) works in the general case (that is, scaling every channel value to a squished interval [Min(p, q), Max(p, q)]), where q is the complement of p (or q = 1 - p) before inverting it (subtracting it from 1), we have the following for a generic channel Ch when partly inverting it:

1 - (q + Ch·(p - q)) = 
= 1 - (1 - p + Ch·(p - (1 - p))) = 
= 1 - (1 - p + Ch·(2·p - 1)) = 
= 1 - (1 - p + 2·Ch·p - Ch) = 
= 1 - 1 + p - 2·Ch·p + Ch = 
= Ch + p - 2·Ch·p

What we got is exactly the formula for exclusion where the other channel is p! Therefore, we can get the same effect as filter: invert(p) for any p in the [0%, 100%] interval by using the exclusion blend mode when the other layer is rgb(p, p, p).

This means we can have gradual inversion along a linear-gradient() that goes from no inversion at all along the left edge, to full inversion along the right edge), with the following:

background: 
  url(butterfly_blues.jpg) 50%/ cover, 
  linear-gradient(90deg, 
    #000 /* equivalent to rgb(0%, 0%, 0%) and hsl(0, 0%, 0%) */, 
    #fff /* equivalent to rgb(100%, 100%, 100%) and hsl(0, 0%, 100%) */);
background-blend-mode: exclusion;
Screenshot of the original butterfly image on the left and the gradually inverted one on the right.
Gradual left-to-right inversion (demo)

Note that using a gradient from black to white for gradual inversion only works with the exclusion blend mode and not with the difference. The result produced by difference in this case, given its formula, is a pseudo gradual inversion that doesn’t pass through the 50% grey in the middle, but through RGB values that have each of the three channels zeroed at various points along the gradient. That is why the contrast looks starker. It’s also perhaps a bit more artistic, but that’s not really something I’m qualified to have an opinion about.

Screenshot of the gradually inverted butterfly image (using the exclusion blend mode) on the left and the pseudo-gradually inverted one (using the difference blend mode) on the right.
Gradual left-to-right inversion vs. pseudo-inversion (demo)

Having different levels of inversion across a background doesn’t necessarily need to come from a black to white gradient. It can also come from a black and white image as the black areas of the image would preserve the background-color, the white areas would fully invert it and we’d have partial inversion for everything in between when using the exclusion blend-mode. difference would again give us a starker duotone result.

This can be seen in the following interactive demo where you can change the background-color and drag the separation line between the results produced by the two blend modes.

Hollow intersection effect

The basic idea here is we have two layers with only black and white pixels.

Ripples and rays

Let’s consider an element with two pseudos, each having a background that’s a repeating CSS gradient with sharp stops:

$d: 15em;
$u0: 10%;
$u1: 20%;

div {
  &::before, &::after {
    display: inline-block;
    width: $d;
    height: $d;
    background: repeating-radial-gradient(#000 0 $u0, #fff 0 2*$u0);
    content: '';
  }
  
  &::after {
    background: repeating-conic-gradient(#000 0% $u1, #fff 0% 2*$u1);
  }
}

Depending on the browser and the display, the edges between black and white may look jagged… or not.

Screenshot showing jagged edges between black and white areas in the two gradients.
Jagged edges (demo)

Just to be on the safe side, we can tweak our gradients to get rid of this issue by leaving a tiny distance, $e, between the black and the white:

$u0: 10%;
$e0: 1px;
$u1: 5%;
$e1: .2%;

div {
  &::before {
    background: 
      repeating-radial-gradient(
        #000 0 calc(#{$u0} - #{$e0}), 
        #fff $u0 calc(#{2*$u0} - #{$e0}), 
        #000 2*$u0);
  }
  
  &::after {
    background: 
      repeating-conic-gradient(
        #000 0% $u1 - $e1, 
        #fff $u1 2*$u1 - $e1, 
        #000 2*$u1);
  }
}
Screenshot showing smoothed edges between black and white areas in the two gradients.
Smooth edges (demo)

Then we can place them one on top of the other and set mix-blend-mode to exclusion or difference, as they both produce the same result here.

div {
  &::before, &::after {
    /* same other styles minus the now redundant display */
    position: absolute;
    mix-blend-mode: exclusion;
  }
}

Wherever the top layer is black, the result of the blending operation is identical to the other layer, whether that’s black or white. So, black over black produces black, while black over white produces white.

Wherever the top layer is white, the result of the blending operation is identical to the other layer inverted. So, white over black produces white (black inverted), while white over white produces black (white inverted).

However, depending on the browser, the actual result we see may look as desired (Chromium) or like the ::before got blended with the greyish background we’ve set on the body and then the result blended with the ::after (Firefox, Safari).

Screenshot collage. On the left, we have the expected black and white result, something like a XOR between the radial gradient generated ripples and the conic gradient generated rays — this is what we get in Chrome. On the right, we have the same result blended with the lightish grey background — this is what we get in Firefox and Safari.
Chromium 87 (left): result looks as desired; Firefox 83 and Safari 14 (right): cloudy from being blended with the body layer (demo)

The way Chromium behaves is a bug, but that’s the result we want. And we can get it in Firefox and Safari, too, by either setting the isolation property to isolate on the parent div (demo) or by removing the mix-blend-mode declaration from the ::before (as this would ensure the blending operation between it and the body remains the default normal, which means no blending) and only setting it on the ::after (demo).

Of course, we can also simplify things and make the two blended layers be background layers on the element instead of its pseudos. This also means switching from mix-blend-mode to background-blend-mode.

$d: 15em;
$u0: 10%;
$e0: 1px;
$u1: 5%;
$e1: .2%;

div {
  width: $d;
  height: $d;
  background: 
    repeating-radial-gradient(
      #000 0 calc(#{$u0} - #{$e0}), 
      #fff $u0 calc(#{2*$u0} - #{$e0}), 
      #000 2*$u0), 
    repeating-conic-gradient(
      #000 0% $u1 - $e1, 
      #fff $u1 2*$u1 - $e1, 
      #000 2*$u1);;
  background-blend-mode: exclusion;
}

This gives us the exact same visual result, but eliminates the need for pseudo-elements, eliminates the potential unwanted mix-blend-mode side effect in Firefox and Safari, and reduces the amount of CSS we need to write.

The desired black and white result, something like a XOR between the radial gradient generated ripples and the conic gradient generated rays.
Desired result with no pseudos (demo)
Split screen

The basic idea is we have a scene that’s half black and half white, and a white item moving from one side to the other. The item layer and the scene layer get then blended using either difference or exclusion (they both produce the same result).

When the item is, for example, a ball, the simplest way to achieve this result is to use a radial-gradient for it and a linear-gradient for the scene and then animate the background-position to make the ball oscillate.

$d: 15em;

div {
  width: $d;
  height: $d;
  background: 
    radial-gradient(closest-side, #fff calc(100% - 1px), transparent) 
      0/ 25% 25% no-repeat,
    linear-gradient(90deg, #000 50%, #fff 0);
  background-blend-mode: exclusion;
  animation: mov 2s ease-in-out infinite alternate;
}

@keyframes mov { to { background-position: 100%; } }
Animated gif. Shows a white ball oscillating left and right and being XORed with the background that's half white (thus making the ball black) and half black (leaving the ball white).
Oscillating ball (demo)

We can also make the ::before pseudo the scene and the ::after the moving item:

$d: 15em;

div {
  display: grid;
  width: $d;
  height: $d;
  
  &::before, &::after {
    grid-area: 1/ 1;
    background: linear-gradient(90deg, #000 50%, #fff 0);
    content: '';
  }
  
  &::after {
    place-self: center start;
    padding: 12.5%;
    border-radius: 50%;
    background: #fff;
    mix-blend-mode: exclusion;
    animation: mov 2s ease-in-out infinite alternate;
  }
}

@keyframes mov { to { transform: translate(300%); } }

This may look like we’re over-complicating things considering that we’re getting the same visual result, but it’s actually what we need to do if the moving item isn’t just a disc, but a more complex shape, and the motion isn’t just limited to oscillation, but it also has a rotation and a scaling component.

$d: 15em;
$t: 1s;

div {
  /* same as before */
  
  &::after {
    /* same as before */
    /* creating the shape, not detailed here as
       it's outside the scope of this article */
    @include poly;
    /* the animations */
    animation: 
      t $t ease-in-out infinite alternate, 
      r 2*$t ease-in-out infinite, 
      s .5*$t ease-in-out infinite alternate;
  }
}

@keyframes t { to { translate: 300% } }
@keyframes r {
  50% { rotate: .5turn; }
  100% { rotate: 1turn;; }
}
@keyframes s { to { scale: .75 1.25 } }
Animated gif. Shows a white triangle oscillating left and right (while also rotating and being squished) and being XORed with the background that's half white (thus making the triangle black) and half black (leaving the triangle white).
Oscillating and rotating plastic shape (demo)

Note that, while Safari has now joined Firefox in supporting the individual transform properties we’re animating here, these are still behind the Experimental Web Platform features flag in Chrome (which can be enabled from chrome://flags as shown below).

Screenshot showing the Experimental Web Platform Features flag being enabled in Chrome.
The Experimental Web Platform features flag enabled in Chrome.
More examples

We won’t be going into details about the “how” behind these demos as the basic idea of the blending effect using exclusion or difference is the same as before and the geometry/animation parts are outside the scope of this article. However, for each of the examples below, there is a link to a CodePen demo in the caption and a lot of these Pens also come with a recording of me coding them from scratch.

Here’s a crossing bars animation I recently made after a Bees & Bombs GIF:

4 squares distributed in a cross pattern turn out to be, two by two, the broken halves of two bars. They rotate back into position and stretch out vertically, then the bars rotate and get XORed to give us the initial cross pattern.
Crossing bars (demo)

And here’s a looping moons animation from a few years back, also coded after a Bees & Bombs GIF:

Animated gif. Shows 12 moons in the last quarter phase distributed on a circle such that they overlap and XOR each other. In that position they then rotate around themselves with a delay depending on their index/ position on a circle, thus making the intersection/XOR pattern rotate as well.
Moons (demo)

We’re not necessarily limited to just black and white. Using a contrast filter with a subunitary value (filter: contrast(.65) in the example below) on a wrapper, we can turn the black into a dark grey and the white into a light grey:

Animated gif. We start with four squares left in the corners of a square out of which we subtracted another inner square whose vertices are on the middle of the edges of the first (outer) square. This turns out two be the result of XOR-ing the inner square with the two triangular halves of the outer square. These triangular halves move out in the direction of their right angle corner, rotate by 45 and shrink until their catheti equal the small square edges and they don't intersect the inner square anymore and the middle of their hypotenuse is perpendicular onto a diagonal of the inner square. The inner square then splits in half along the other diagonal and the halves move out in the direction of their right angle corner until we get the initial shape again.
Discovery: two squares/ four triangles (demo, source)

Here’s another example of the same technique:

Animated gif. We start with the 8 triangles that result when we XOR two squares rotated at 45. Triangles 1, 2, 5, 6 move inwards forming two squares rotated at 45 which, when XORed, give us the initial shape again. The other triangles move out and disappear.
Eight triangles (demo, source)

If we want to make it look like we have a XOR effect between black shapes on a white background, we can use filter: invert(1) on the wrappers of the shapes, like in the example below:

Animated gif. We start with 4 bars on the outside of a square. These bars move inwards until opposing ones touch. XORing them gives us the initial shape again.
Four bars (demo, source)

And if we want something milder like dark grey shapes on a light grey background, we don’t go for full inversion, but only for partial one. This means using a subunitary value for the invert filter like in the example below where we use filter: invert(.85):

Animated gif. We start with the 6 triangles we get when out of a 6 point star we subtract the hexagon formed by its 6 inner vertices. 2 opposing triangles out of these 6 grow and move inwards to intersect eventually giving us the same shape as we had initially, while the other 4 move out and shrink to nothing.
Six triangles (demo, source)

It doesn’t necessarily have to be something like a looping or loading animation. We can also have a XOR effect between an element’s background and its offset frame. Just like in the previous examples, we use CSS filter inversion if we want the background and the frame to be black and their intersection to be white.

Screenshot. Shows square boxes of text XORed with their offset frames that use the same `color` as the `background-color` of the box (either black or white).
Offset and XOR frame (demo).

Another example would be having a XOR effect on hovering/ focusing and clicking a close button. The example below shows both night and light theme cases:

Bring me to life

Things can look a bit sad only in black and white, so there are few things we can do to put some life into such demos.

The first tactic would be to use filters. We can break free from the black and white constraint by using sepia() after lowering the contrast (as this function has no effect over pure black or white). Pick the hue using hue-rotate() and then fine tune the result using brightness() and saturate() or contrast().

For example, taking one of the previous black and white demos, we could have the following filter chain on the wrapper:

filter: 
  contrast(.65) /* turn black and white to greys */
  sepia(1) /* retro yellow-brownish tint */
  hue-rotate(215deg) /* change hue from yellow-brownish to purple */
  blur(.5px) /* keep edges from getting rough/ jagged */
  contrast(1.5) /* increase saturation */
  brightness(5) /* really brighten background */
  contrast(.75); /* make triangles less bright (turn bright white dirty) */
We start with four dirty white squares on a purple background. These four squares are what's left in the corners of a square out of which we subtracted another inner square whose vertices are on the middle of the edges of the first (outer) square. This turns out two be the result of XOR-ing the inner square with the two triangular halves of the outer square. These triangular halves move out in the direction of their right angle corner, rotate by 45 and shrink until their catheti equal the small square edges and they don't intersect the inner square anymore and the middle of their hypotenuse is perpendicular onto a diagonal of the inner square. The inner square then splits in half along the other diagonal and the halves move out in the direction of their right angle corner until we get the initial shape again.
Discovery: two squares/four triangles — a more lively version (demo)

For even more control over the result, there’s always the option of using SVG filters.

The second tactic would be to add another layer, one that’s not black and white. For example, in this radioactive pie demo I made for the first CodePen challenge of March, I used a purple ::before pseudo-element on the body that I blended with the pie wrapper.

body, div { display: grid; }

/* stack up everything in one grid cell */
div, ::before { grid-area: 1/ 1; }

body::before { background: #7a32ce; } /* purple layer */

/* applies to both pie slices and the wrapper */
div { mix-blend-mode: exclusion; }

.a2d { background: #000; } /* black wrapper */

.pie {
  background: /* variable size white pie slices */
    conic-gradient(from calc(var(--p)*(90deg - .5*var(--sa)) - 1deg), 
      transparent, 
      #fff 1deg calc(var(--sa) + var(--q)*(1turn - var(--sa))), 
      transparent calc(var(--sa) + var(--q)*(1turn - var(--sa)) + 1deg));
}

This turns the black wrapper purple and the white parts green (which is purple inverted).

Animated gif. Starts out with 9 pies all stacked one on top of the other and XORed (XORing an odd number of identical layers outputs a layer just like the input ones). They gradually slide out and get reduced to a slice that's one ninth of the pie, then slide back in order to together form a full pie again. Then it all repeats itself.
Radioactive 🥧 slices (demo)

Another option would be blending the entire wrapper again with another layer, this time using a blend mode different from difference or exclusion. Doing so would allow us more control over the result so we’re not limited to just complementaries (like black and white, or purple and green). That, however, is something we’ll have to cover in a future article.

Finally, there’s the option of using difference (and not exclusion) so that we get black where two identical (not necessarily white) layers overlap. For example, the difference between coral and coral is always going to be 0 on all three channels, which means black. This means we can adapt a demo like the offset and XOR frame one to get the following result:

Screenshot. Shows square boxes of text XORed with their offset frames that use the same `color` as the `background-color` of the box (not necessarily black or white).
Offset and XOR frame — a more lively version (demo).

With some properly set transparent borders and background clipping, we can also make this work for gradient backgrounds:

Screenshot. Shows square boxes of text XORed with their offset frames that use the same gradient as the background of the box.
Offset and XOR frame example — a gradient version (demo).

Similarly, we can even have an image instead of a gradient!

Screenshot. Shows square boxes of text XORed with their offset frames that use the same image as the background of the box.
Offset and XOR frame — an image version (demo).

Note that this means we also have to invert the image background when we invert the element in the second theme scenario. But that should be no problem, because in this article we’ve also learned how to do that: by setting background-color to white and blending the image layer with it using background-blend-mode: exclusion!

Closing thoughts

Just these two blend modes can help us get some really cool results without resorting to canvas, SVG or duplicated layers. But we’ve barely scratched the surface here. In future articles, we’ll dive into how other blend modes work and what we can achieve with them alone or in combination with previous ones or with other CSS visual effects such as filters. And trust me, the more tricks you have up your sleeve, the cooler the results you’re able to achieve get!


Taming Blend Modes: `difference` and `exclusion` originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/taming-blend-modes-difference-and-exclusion/feed/ 2 334463
Weaving One Element Over and Under Another Element https://css-tricks.com/weaving-one-element-over-and-under-another-element/ https://css-tricks.com/weaving-one-element-over-and-under-another-element/#comments Mon, 14 Oct 2019 18:46:59 +0000 https://css-tricks.com/?p=295860 In this post, we’re going to use CSS superpowers to create a visual effect where two elements overlap and weave together. The epiphany for this design came during a short burst of spiritual inquisitiveness where I ended up at The


Weaving One Element Over and Under Another Element originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
In this post, we’re going to use CSS superpowers to create a visual effect where two elements overlap and weave together. The epiphany for this design came during a short burst of spiritual inquisitiveness where I ended up at The Bible Project’s website. They make really cool animations, and I mean, really cool animations.

My attention, however, deviated from spiritualism to web design as I kept spotting these in-and-out border illustrations.

Screenshot form The Bible Project website.

I wondered if a similar could be made from pure CSS… and hallelujah, it’s possible!

See the Pen
Over and under border design using CSS
by Preethi Sam (@rpsthecoder)
on CodePen.

The principal CSS standards we use in this technique are CSS Blend Modes and CSS Grid.

First, we start with an image and a rotated frame in front of that image.

<div class="design">
  <img src="bird-photo.jpg">
  <div class="rotated-border"></div>
</div>
.design {
  position: relative;
  height: 300px;
  width: 300px;
}

.design > * {
  position: absolute;
  height: 100%;
  width: 100%;
}

.rotated-border {
  box-sizing: border-box;
  border: 15px #eb311f solid;
  transform: rotate(45deg);
  box-shadow: 0 0 10px #eb311f, inset 0 0 20px #eb311f;
}

The red frame is created using border. Its box-sizing is set to include the border size in the dimensions of the box so that the frame is centered around the picture after being rotated. Otherwise, the frame will be bigger than the image and get pulled towards the bottom-right corner.

Then we pick a pair of opposite corners of the image and overlay their quadrants with their corresponding portion in a copy of the same image as before. This hides the red frame in those corners.

We basically need to make a cut portion of the image that looks like below to go on top of the red frame.

The visible two quadrants will lay on top of the .rotated-border element.

So, how do we alter the image so that only two quadrants of the image are visible? CSS Blend Modes! The multiply value is what we’re going to reach for in this instance. This adds transparency to an element by stripping white from the image to reveal what’s behind the element.

Chris has a nice demo showing how a red background shows through an image with the multiply blend mode.

See the Pen
Background Blending
by Chris Coyier (@chriscoyier)
on CodePen.

OK, nice, but what about those quadrants? We cover the quadrants we want to hide with white grid cells that will cause the image to bleed all the way through in those specific areas with a copy of the bird image right on top of it in the sourcecode.

<div id="design">
    <img src="bird-photo.jpg">
    <div class="rotated-border"></div>

    <div class="blend">
      <!-- Copy of the same image -->
      <img src="bird-photo.jpg">
      <div class="grid">
        <!-- Quadrant 1: Top Left -->
        <div></div>
        <!-- Quadrant 2: Top Right -->
        <div data-white></div>
        <!-- Quadrant 3: Bottom Left -->
        <div data-white></div>
        <!-- Quadrant 4: Bottom Right -->
        <div></div>
      </div>
    </div>

</div>
.blend > * {
  position: absolute;
  height: 100%;
  width: 100%;
}

/* Establishes our grid */
.grid {
  display: grid;
  grid: repeat(2, 1fr) / repeat(2, 1fr);
}

/* Adds white to quadrants with this attribute */
[data-white]{
  background-color: white;
}

The result is a two-by-two grid with its top-right and bottom-left quadrants that are filled with white, while being grouped together with the image inside .blend.

To those of you new to CSS Grid, what we’re doing is adding a new .grid element that becomes a “grid” element when we declare display: grid;. Then we use the grid property (which is a shorthand that combines grid-template-columns and grid-template-rows) to create two equally spaced rows and columns. We’re basically saying, “Hey, grid, repeat two equal columns and repeat two equal rows inside of yourself to form four boxes.”

A copy of the image and a grid with white cells on top of the red border.

Now we apply the multiply blend mode to .blend using the mix-blend-mode property.

.blend { mix-blend-mode: multiply; }

The result:

As you can see, the blend mode affects all four quadrants rather than just the two we want to see through. That means we can see through all four quadrants, which reveals all of the red rotated box.

We want to bring back the white we lost in top-left and bottom-right quadrants so that they hide the red rotated box behind them. Let’s add a second grid, this time on top of .blend in the sourcecode.

<div id="design">
  <img src="bird-photo.jpg">
  <div class="rotated-border"></div>
    
  <!-- A second grid  -->
  <!-- This time, we're adding white to the image quandrants where we want to hide the red frame  -->
  <div class="grid">
    <!-- Quadrant 1: Top Left -->
    <div data-white></div>
    <!-- Quadrant 2: Top Right -->
    <div></div>
    <!-- Quadrant 3: Bottom Left -->
    <div></div>
    <!-- Quadrant 4: Bottom Right -->
    <div data-white></div>
  </div>

  <div class="blend">
    <img src="bird-photo.jpg">
    <div class="grid">
      <!-- Quadrant 1: Top Left -->
      <div></div>
      <!-- Quadrant 2: Top Right -->
      <div data-white></div>
      <!-- Quadrant 3: Bottom Left -->
      <div data-white></div>
      <!-- Quadrant 4: Bottom Right -->
      <div></div>
    </div>
  </div>

</div>

The result!

Summing up, the browser renders the elements in our demo like this:
​​

  1. ​​At bottommost is the bird image (represented by the leftmost grey shape in the diagram below)
  2. ​​Then a rotated red frame
  3. ​​On top of them is a grid with top-left and bottom-right white cells (corners where we don’t want to see the red frame in the final result)
  4. ​​Followed by a copy of the bird image from before and a grid with top-right and bottom-left white cells (corners where we do want to see the red frame) – both grouped together and given the blending mode, multiply​.

You may have some questions about the approach I used in this post. Let me try to tackle those.

What about using CSS Masking instead of CSS Blend Modes?

For those of you familiar with CSS Masking – using either mask-image or clip-path – it can be an alternative to using blend mode.

I prefer blending because it has better browser support than masks and clipping. For instance, WebKit browsers don’t support SVG <mask> reference in the CSS mask-image property and they also provide partial support for clip-path values, especially Safari.

Another reason for choosing blend mode is the convenience of being able to use grid to create a simple white structure instead of needing to create images (whether they are SVG or otherwise).

Then again, I’m fully on board the CSS blend mode train, having used it for knockout text, text fragmentation effect… and now this. I’m pretty much all in on it.

Why did you use grid for the quadrants?

The white boxes needed in the demo can be created by other means, of course, but grid makes things easier for me. For example, we could’ve leaned on flexbox instead. Use what works for you.

Why use a data-attribute on the grid quadrant elements to make them white?

I used it while coding the demo without thinking much about it – I guess it was quicker to type. I later thought of changing it to a class, but left it as it is because the HTML looked neater that way… at least to me. :)

Is multiply the only blend mode that works for this example?

Nope. If you already know about blend modes then you probably also know you can use either screen, darken, or lighten to get a similar effect. (Both screen and lighten will need black grid cells instead of white.)


Weaving One Element Over and Under Another Element originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/weaving-one-element-over-and-under-another-element/feed/ 19 295860
Recreating the Facebook Messenger Gradient Effect with CSS https://css-tricks.com/recreating-the-facebook-messenger-gradient-effect-with-css/ https://css-tricks.com/recreating-the-facebook-messenger-gradient-effect-with-css/#comments Fri, 01 Mar 2019 16:01:34 +0000 http://css-tricks.com/?p=283491 One Sunday morning, I woke up a little earlier than I would’ve liked to, thanks to the persistent buzzing of my phone. I reached out, tapped into Facebook Messenger, and joined the conversation. Pretty soon my attention went from the …


Recreating the Facebook Messenger Gradient Effect with CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
One Sunday morning, I woke up a little earlier than I would’ve liked to, thanks to the persistent buzzing of my phone. I reached out, tapped into Facebook Messenger, and joined the conversation. Pretty soon my attention went from the actual conversations to the funky gradient effect of the message bubbles containing them. Let me show you what I mean:

This is a new feature of Messenger, which allows you to choose a gradient instead of a plain color for the background of the chat messages. It’s currently available on the mobile application as well as Facebook’s site, but not yet on Messenger’s site. The gradient appears “fixed” so that chat bubbles appear to change background color as they scroll vertically.

I thought this looked like something that could be done in CSS, so… challenge accepted!

Let’s walk through my thought process as I attempted to recreate it and explain the CSS features that were used to make it work. Also, we’ll see how Facebook actually implemented it (spoiler alert: not the way I did) and how the two approaches compare.

Getting our hands dirty

First, let’s look at the example again to see what exactly it is that we’re trying to achieve here.

In general, we have a pretty standard messaging layout: messages are divided into bubbles going from top to bottom, ours on the right and the other people in the chat on the left. The ones on the left all have a gray background color, but the ones on the right look like they’re sharing the same fixed background gradient. That’s pretty much it!

Step 1: Set up the layout

This part is pretty simple: let’s arrange the messages in an ordered list and apply some basic CSS to make it look more like an actual messaging application:

<ol class="messages">
  <li class="ours">Hi, babe!</li>
  <li class="ours">I have something for you.</li>
  <li>What is it?</li>
  <li class="ours">Just a little something.</li>
  <li>Johnny, it’s beautiful. Thank you. Can I try it on now?</li>
  <li class="ours">Sure, it’s yours.</li>
  <li>Wait right here.</li>
  <li>I’ll try it on right now.</li>
</ol>

When it comes to dividing the messages to the left and the right, my knee-jerk reaction was to use floats. We could use float: left for messages on the left and float: right for messages on the right to have them stick to different edges. Then, we’d apply clear: both to on each message so they stack. But there’s a much more modern approach — flexbox!

We can use flexbox to stack the list items vertically with flex-direction: column and tell all the children to stick to the left edge (or “align the cross-start margin edges of the flex children with cross-start margin edges of the lines,” if you prefer the technical terms) with align-items: flex-start. Then, we can overwrite the align-items value for individual flex items by setting align-self: flex-end on them.

What, you mean you couldn’t visualize the code based on that? Fine, here’s how that looks:

.messages {
  /* Flexbox-specific styles */
  display: flex;
  flex-direction: column;
  align-items: flex-start;

  /* General styling */
  font: 16px/1.3 sans-serif;
  height: 300px;
  list-style-type: none;
  margin: 0 auto;
  padding: 8px;
  overflow: auto;
  width: 200px;
}

/* Default styles for chat bubbles */
.messages li {
  background: #eee;
  border-radius: 8px;
  padding: 8px;
  margin: 2px 8px 2px 0;
}

/* Styles specific to our chat bubbles */
.messages li.ours {
  align-self: flex-end; /* Stick to the right side, please! */
  margin: 2px 0 2px 8px;
}

Some padding and colors here and there and this already looks similar enough to move on to the fun part.

Step 2: Let’s color things in!

The initial idea for the gradient actually came to me from this tweet by Matthias Ott (that Chris recreated in another post):

The key clue here is mix-blend-mode, which is a CSS property that allows us to control how the content of an element blends in with what’s behind it. It’s a feature that has been present in Photoshop and other similar tools for a while, but is fairly new to the web. There’s an almanac entry for the property that explains all of its many possible values.

One of the values is screen: it takes the values of the pixels of the background and foreground, inverts them, multiplies them, and inverts them once more. This results in a color that is brighter than the original background color.

The description can seem a little confusing, but what it essentially means is that if the background is monochrome, wherever the background is black, the foreground pixels are shown fully and wherever it is white, white remains.

With mix-blend-mode: screen; on the foreground, we’ll see more of the foreground as the background is darker.

So, for our purposes, the background will be the chat window itself and the foreground will contain an element with the desired gradient set as the background that’s positioned over the background. Then, we apply the appropriate blend mode to the foreground element and restyle the background. We want the background to be black in places where we want the gradient to be shown and white in other places, so we’ll style the bubbles by giving them a plain black background and white text. Oh, and let’s remember to add pointer-events: none to the foreground element so the user can interact with the underlying text.

At this point, I also changed the original HTML a little. The entire chat is a wrapper in an additional container that allows the gradient to stay “fixed” over the scrollable part of the chat:

.messages-container:after {
  content: '';
  background: linear-gradient(rgb(255, 143, 178) 0%, rgb(167, 151, 255) 50%, rgb(0, 229, 255) 100%);
  position: absolute;
  left: 0;
  top: 0;
  height: 100%;
  width: 100%;
  mix-blend-mode: screen;
  pointer-events: none;
}

.messages li {
  background: black;
  color: white;
  /* rest of styles */
}

The result looks something like this:

The gradient applied to the chat bubbles

Step 3: Exclude some messages from the gradient

Now the gradient is being shown where the text bubbles are under it! However, we only want it to be shown over our bubbles — the ones along the right edge. A hint to how that can be achieved is hidden in MDN’s description of the mix-blend-mode property:

The mix-blend-mode CSS property sets how an element’s content should blend with the content of the element’s parent and the element’s background.

That’s right! The background. Of course, the effect only takes into account the HTML elements that are behind the current element and have a lower stack order. Fortunately, the stacking order of elements can easily be changed with the z-index property. So all we have to do is to give the chat bubbles on the left a higher z-index than that of the foreground element and they will be raised above it, outside of the influence of mix-blend-mode! Then we can style them however we want.

The gradient applied to the chat bubbles.

Let’s talk browser support

At the time of writing, mix-blend-mode is not supported at all in Internet Explorer and Edge. In those browsers, the gradient is laid over the whole chat and others’ bubbles appear on top of it, which is not an ideal solution.

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

Desktop

ChromeFirefoxIEEdgeSafari
4132No79TP

Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
11511511517

So, this is what we get in unsupported browsers:

How browsers that don’t support mix-blend-mode render the chat.

Fortunately, all the browsers that support mix-blend-mode also support CSS Feature Queries. Using them allows us to write fallback styles for unsupported browsers first and include the fancy effects for the browsers that support them. This way, even if a user can’t see the full effect, they can still see the whole chat and interact with it:

A simplified UI for older browsers, falling back to a plain cyan background color.

Here’s the final Pen with the full effect and fallback styles:

See the Pen
Facebook Messenger-like gradient coloring in CSS
by Stepan Bolotnikov (@Stopa)
on CodePen.

Now let’s see how Facebook did it

Turns out that Facebook’s solution is almost the opposite of what we’ve covered here. Instead of laying the gradient over the chat and cutting holes in it, they apply the gradient as a fixed background image to the whole chat. The chat itself is filled with a whole bunch of empty elements with white backgrounds and borders, except where the gradient should be visible.

The final HTML rendered by the Facebook Messenger React app is pretty verbose and hard to navigate, so I recreated a minimal example to demonstrate it. A lot of the empty HTML elements can be switched for pseudo-elements instead:

See the Pen
Facebook Messenger-like gradient coloring in CSS: The Facebook Way
by Stepan Bolotnikov (@Stopa)
on CodePen.

As you can see, the end result looks similar to the mix-blend-mode solution, but with a little bit of extra markup. Additionally, their approach provides more flexibility for rich content, like images and emojis . The mix-blend-mode approach doesn’t really work if the background is anything but monochrome and I haven’t been able to come up with a way to “raise” inner content above the gradient or get around this limitation in another way.

Because of this limitation, it’s wiser to use Facebook’s approach in an actual chat application. Still, our solution using mix-blend-mode showcases an interesting way to use one of the most under-appreciated CSS properties in modern web design and hopefully it has given you some ideas on what you could do with it!


Recreating the Facebook Messenger Gradient Effect with CSS originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/recreating-the-facebook-messenger-gradient-effect-with-css/feed/ 8 283491
Multi-Line Inline Gradient https://css-tricks.com/multi-line-inline-gradient/ https://css-tricks.com/multi-line-inline-gradient/#comments Thu, 03 Jan 2019 15:17:15 +0000 http://css-tricks.com/?p=280200 Came across this thread:


Multi-Line Inline Gradient originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
Came across this thread:

My first thought process was:

But it turns out we need a litttttle extra trickery to make it happen.

If a solid color is fine, then some padding combined with box-decoration-break should get the basic framework:

See the Pen Multiline Padding with box-decoration-break by Chris Coyier (@chriscoyier) on CodePen.

But a gradient on there is gonna get weird on multiple lines:

See the Pen Multiline Padding with box-decoration-break by Chris Coyier (@chriscoyier) on CodePen.

I’m gonna credit Matthias Ott, from that thread, with what looks like the perfect answer to me:

See the Pen Multiline background gradient with mix-blend-mode by Matthias Ott (@matthiasott) on CodePen.

The trick there is to set up the padded multi-line background just how you want it with pure white text and a black background. Then, a pseudo-element is set over the whole area with the gradient in the black area. Throw in mix-blend-mode: lighten; to make the gradient only appear on the black area. Nice one.


Multi-Line Inline Gradient originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/multi-line-inline-gradient/feed/ 9 280200
mix-blend-mode https://css-tricks.com/almanac/properties/m/mix-blend-mode/ https://css-tricks.com/almanac/properties/m/mix-blend-mode/#comments Sun, 12 Apr 2015 14:49:26 +0000 http://css-tricks.com/?page_id=200237 The mix-blend-mode property defines how an element’s content should blend with its background. For example, the text of a <h1 could blend with the background behind it in interesting ways.

.blend {
  mix-blend-mode: exclusion;
}

CodePen Embed Fallback

In the …


mix-blend-mode originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
The mix-blend-mode property defines how an element’s content should blend with its background. For example, the text of a <h1> could blend with the background behind it in interesting ways.

.blend {
  mix-blend-mode: exclusion;
}

In the example above the content has been modified by the mix-blend-mode so that the colors of the text are excluded from its background. This is just one of many values for this property.

There is an issue with Chrome 58+ where mix-blend-mode will not render on elements that are set on a transparent . The has been ticketed as Issue 711955 in Chrome, which is assigned at the time of this writing. In the meantime, a simple fix is to assign a white (or really, any) background color to the body element.

Values

  • initial: the default setting of the property that does not set a blend mode.
  • inherit: an element will inherit the blend mode from its parent element.
  • unset: removes the current blend mode from an element.
  • <blend-mode>: this is the attribute of one of the blend modes beneath:
  • normal: this attribute applies no blending whatsoever.
  • multiply: the element is multiplied by the background and replaces the background color. The resulting color is always as dark as the background.
  • screen: multiplies the background and the content then complements the result. This will result in content which is brighter than the background-color.
  • overlay: multiplies or screens the content depending on the background color. This is the inverse of the hard-light blend mode.
  • darken: the background is replaced with the content where the content is darker, otherwise, it is left as it was.
  • lighten: the background is replaced with the content where the content is lighter.
  • color-dodge: this attribute brightens the background color to reflect the color of the content.
  • color-burn: this darkens the background to reflect the content’s natural color.
  • hard-light: depending on the color of the content this attribute will screen or multiply it.
  • soft-light: depending on the color of the content this will darken or lighten it.
  • difference: this subtracts the darker of the two colors from the lightest color.
  • exclusion: similar to difference but with lower contrast.
  • hue: creates a color with the hue of the content combined with the saturation and luminosity of the background.
  • saturation: creates a color with the saturation of the content combined with the hue and luminosity of the background.
  • color: creates a color with the hue and saturation of the content and the luminosity of the background.
  • luminosity: creates a color with the luminosity of the content and the hue and saturation of the background. This is the inverse of the color attribute.

It’s worth noting that setting a blend mode other than normal will generate a new stacking context which must then be blended with the stacking context that contains the element.

The effect of these values are shown in the demo below:

More information

Browser Support

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

Desktop

ChromeFirefoxIEEdgeSafari
4132No79TP

Mobile / Tablet

Android ChromeAndroid FirefoxAndroidiOS Safari
11511511517

mix-blend-mode originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

]]>
https://css-tricks.com/almanac/properties/m/mix-blend-mode/feed/ 30 200237