As people who use HTML and CSS to build things for the web, we run into difficult challenges daily. Sometimes things seem so easy but end up being a real pain in the butt. Well, I’m here to help… at least a little bit… well, hopefully. In this post, I’m going to show you how I’ve solved three pretty common design issues with CSS masks in the past.
First, we’re going to add fade-out effects to the top and bottom of a scrolling container.
Then, we’re going to add an irregular shape to the edges of a banner.
And after that, we’ll use a mask image to pull off a hexagonal style for a square image.
Now, if that doesn’t get you pumped, I guess I don’t know what will! Alright, let’s go.
mask-image
ExplainedSo, before we get too far ahead of ourselves, we need to understand what we’re doing and how it actually works. We’re going to be using the CSS mask-image
property. The rules and syntax for mask-image
will probably look a little familiar. They are a lot like the CSS background-image
properties.
Masks in CSS work by using a combination of opaque and transparent areas. The areas that are completely transparent will mask the things behind them. The areas that are completely opaque will allow the things behind them to be seen.
And areas that are partially opaque will allow the things behind them to be partially seen.
Okay, so how about an example?
mask-image
with linear-gradients
to Create a Fade Effect for a Scrolling ContainerOur goal here is to create a fade-out effect at the top and bottom of our scrolling container, like what we're seeing here.
So, based on what we just learned about masks, you probably have some ideas about what we need. We need a mask that is an opaque color for most of the container but that fades to transparent at the top and then again at the bottom.
So, if we think about what we need, we want to place a mask on top of our scrolling container, and it's going to fade at the top and bottom. For this, we're going to need to use two linear-gradients
with our mask-image
property. One gradient will fade from fully opaque black at the top down to the bottom, where it'll need to fade to transparent. And then vice versa, at the top, it'll fade from fully opaque at the bottom to transparent at the top.
So, let's start with the bottom first. To do this, we'll add our mask-image
property. And we're going to want to add a linear-gradient
. Since we're doing the bottom first, we will make this gradient go "to bottom". Also, we're going to want the fades to be 5 em tall, so we'll add a custom property for this so that it can be used multiple times.
:root {
--fadeHeight: 5em;
}
.container {
mask-image: linear-gradient(to bottom)
}
So, to start, we're going to start with fully opaque black, and then it's going to be fully opaque black for the height of the entire container until we get down to where we want the fade to start. For this, we'll use a calculation, and then the final piece is to make it transparent at the bottom.
.container {
mask-image:
linear-gradient(
to bottom,
black,
black calc(100% - var(--fadeHeight)),
transparent
)
}
Now, we can see that we have our fade effect at the bottom.
Now we need to add the top, so we'll add another linear-gradient
. This time, we're going to start from the bottom and go upward, so we'll add "to top", and it will start as fully opaque black.
This gradient will be the opposite of the bottom fade. It will be 100 percent of the height minus the height of the fade, so we'll use a calculation again.
.container {
mask-image:
linear-gradient(
to top,
black,
black calc(100% - var(--fadeHeight)),
transparent
),
linear-gradient(
to bottom,
black,
black calc(100% - var(--fadeHeight)),
transparent
)
}
But, at this point, we've actually broken our mask. It's no longer fading at the bottom like we'd expect.
Why is this happening?
Well, if we think about what we did, we just applied two masks that were exactly overlapping each other. And this means that there's now a fully opaque black mask applied to the whole container. What we really need to do is, in both of these linear-gradients
, we need to make room for our fades at the top and bottom.
So, on our bottom mask, we need to make it transparent before it becomes black to make room for the fade at the top. It will need to remain transparent with the height of the fade so we can use our --fadeHeight
custom property. We also need to do the same to top fade linear-gradient
to make room for the fade at the bottom.
.container {
mask-image:
linear-gradient(
to top,
transparent,
transparent var(--fadeHeight),
black,
black calc(100% - var(--fadeHeight)),
transparent
),
linear-gradient(
to bottom,
transparent,
transparent var(--fadeHeight),
black,
black calc(100% - var(--fadeHeight)),
transparent
)
}
Okay, so how’d we do?
Perfect, fading in both directions now.
One last thing to note here is that I'm actually using Autoprefixer in this Codepen example, which automatically handles vendor-specific prefixes for me. If I wasn't using Autoprefixer, since I'm using Chrome, I would actually need to use the -webkit
prefix on the mask image property for this to work correctly.
-webkit-mask-image
But since I'm using Autoprefixer, it makes it so that I don't need to do this. Just one thing to note as you're adding masks in your own projects.
Here's the final working example:
linear-gradient
, and mask-image
to Irregular Shaped EdgesNow, we're going to move on to another common design concept: containers with an irregular edge of some sort. For this we'll need more than just a linear gradient, we'll also need an SVG. In this example, we're going to use two SVGs that look like this.
And for this example, We have a header that looks like this:
And, we have a footer that looks like this:
Let's start with our header. As we've seen, mask-image
is very much like background-image
, meaning we can use linear-gradients
and we can also use actual images. In this case, we're going to use a combination of an SVG, the url()
function, and a linear-gradient
to mask everything else.
We'll start with the url()
function. In this example, we'll use an encoded SVG, which is a lot like a base64 encoded image. You really rarely should do this type of thing for performance reasons, but for this example, it will work just fine within our Codepen Demo.
header {
mask-image:
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 1000 ...")
}
mask-size
PropertyOkay, now we need to add a mask-size
for this so that it's the proper size. Just like our previous example, we need a couple of custom properties for this, too, because we're going to use them over and over throughout this example.
:root {
--maskWidth: 62.5em; /* 1000px */
--maskHeight: 3.125em; /* 50px */
}
Now, just like background-size
we have a mask-size
property. We'll use our custom properties for this size. For the width, we'll use --maskWidth
and for the height we'll use --maskHeight
.
header {
...
mask-size: var(--maskWidth) var(--maskHeight);
}
mask-position
PropertyNext, we want our image to be positioned on the bottom and in the center, so just like background-position
we have a mask-position
property. And, we'll want to use "bottom" and "center" for this position.
header {
...
mask-position: bottom center;
}
mask-repeat
PropertyNow, we want to make sure that we're not repeating the image along the y-axis
and only along the x-axis
. So just like background-repeat
, we have a mask-repeat
property. And we'll use a value of repeat-x
.
header {
...
mask-repeat: repeat-x;
}
There we go; it's starting to take shape.
So now we need to use our linear-gradient
to mask everything above this image out so that we can see our background. We'll start at the top with the value of fully opaque black. Then we'll want it to be black all the way until it meets up with the top of the mask image, so again, we're going to use a calculation. This calculation will be 100 percent minus the height of our mask. And we'll add a half-pixel value to deal with pixel rounding issues that can sometimes occur. This just helps ensure that the gradient will meet up or overlap the SVG by one pixel. Then, at the same calculation, we'll want our mask to be transparent and then transparent again at 100 percent.
header {
...
mask-image:
...
linear-gradient(
black,
black calc(100% - var(--maskHeight) + 0.5px),
transparent calc(100% - var(--maskHeight) + 0.5px),
transparent
);
}
Now, the only other piece that we're missing is a mask-size
for our linear-gradient
. In this case, it's going to be 100 percent.
header {
...
mask-size: var(--maskWidth) var(--maskHeight), 100%;
}
This makes it spread out 100 percent of the width and 100 percent of the height of this rectangle.
Looking pretty good, right? Now, we can do the same thing to our footer. The only differences are we're using a different image and the gradient value. The image is flipped in the opposite direction of our header image, facing upward. And then our gradient starts at transparent the height of the mask-image
, and then it goes to black the rest of the way. It's also positioned at the top and center.
footer {
mask-image:
url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' ..."),
linear-gradient(
transparent 0%,
transparent var(--maskHeight),
black var(--maskHeight),
black 100%
);
mask-size: var(--maskWidth) var(--maskHeight), 100%;
mask-position: top center;
mask-repeat: repeat-x;
}
So, this gives us a nice way to apply different effects to boxes. Rather than just an ordinary straight line, we can give them more interesting effects with CSS masks.
Here's the final working example:
mask-image
to Create a Hexagonal Shaped ImageNow, in this last example, we're going to mask a square image with a custom shape to provide a more interesting effect. In this case, we'll only need an SVG, no linear gradient. The SVG we're going to use will look like this.
Okay, what's different about this approach is we don't need the linear-gradient
. All we need is the mask-image
and our url()
function. Let's add in our encoded SVG.
img {
mask-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' ...");
}
Okay, not exactly what we want, this mask looks a little funky. What we need to do is set this so that it doesn't repeat with the mask-repeat
property. Also, it's a little off-center, so we need to center it with the mask-position
property.
img {
...
mask-repeat: no-repeat;
mask-position: center;
}
And there we go. It's that simple.
Here's the final working example:
Just to recap, masks are simply created by using some level of opacity and transparency to create the desired effect. We can use linear and radial gradients to create interesting masking effects without the need for images. We can use combinations of CSS gradients in combination with SVG paths or other images to create boxes with irregular edges. We can also use SVGs that are exactly the shape we want and then create a mask from them.
I hope these examples provide some inspiration for what’s possible when using CSS masks. Your creativity is all that’s holding you back now.
Also published here.