I was working on a project that had this neat jagged edge along the bottom of a banner image.
It’s something that made me think for a second and I learned something in the process! I thought I’d write up how I approached it so you can use it on your own projects.
I started out with a good old fashioned HTML image in a wrapper element:
<div class="jagged-wrapper">
<img src="path-to-image.jpg" />
</div>
Then I used its ::after
pseudo element to drop a repeating background image on it:
.jagged-wrapper::after {
content: "";
background-image: url('data:image/svg+xml;utf-8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1" preserveAspectRatio="none"><polygon style="fill:white;" points="1,0 1,1 0,1 "/></svg>');
background-size: 30px 30px;
width: 100%;
height: 30px;
position: absolute;
bottom: 0px;
right: 0;
z-index: 2;
}
That background image? It’s SVG code converted to a Data URI. Here’s the original SVG code. Chris has a nice video where he walks through that conversion.
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1 1" preserveAspectRatio="none">
<polygon style="fill: white;" points="1,0 1,1 0,1 "/>
</svg>
“There we go!” I thought.
While that certainly works, geez, that’s a lot of hassle. It’s difficult to read SVG markup in CSS like that. Plus, it’s annoying to have to remember to quote them (e.g. url('data:image/svg+xml'...)
). Sure, we can base64 encode the SVG to avoid that, but that’s even more annoying. Plus, the SVG needs to be filled with the same background color as the image (or wherever it is used), or else it won’t work.
Wait, isn’t this what masking is for? Yes! Yes, this is what masking is for.
That led me to a new approach: use an image like the one above as a CSS mask so that the “missing” bits of the banner image would actually be missing. Rather than drawing triangles of the background color on top of the banner, we should instead mask away those triangles from the banner entirely and let the real background show through. That way, it works on any background!
Masking is pretty much supported everywhere — at least in the simple way I’m talking about here. We’re also talking about something that can be implemented with progressive enhancement; if masks aren’t supported in a given browser, then you just don’t get the sawtooth effect. Definitely not the end of the world.
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
Chrome | Firefox | IE | Edge | Safari |
---|---|---|---|---|
120 | 53 | No | 120 | 15.4 |
Mobile / Tablet
Android Chrome | Android Firefox | Android | iOS Safari |
---|---|---|---|
122 | 123 | 122 | 15.4 |
One way a CSS mask works is to provide an image with an alpha channel as a mask-image
. The underlying element — the one that’s being masked — becomes (semi-)transparent to the degree that the alpha channel of the mask-image
dictates. So if your mask image is a white teapot on a transparent background, then the masked element will be cut to the shape of the teapot and everything outside that will be hidden.
Masking can be a tricky concept to grok. Sarah Drasner has an article that takes a deep-dive on masking, including how it is different from clipping. There’s much more that masks can do than what we’re covering here. Check the specs, caniuse, and MDN for even more information.
What we need is a single “sawtooth”-like image similar to the SVG above, where the top-left half is filled white and the bottom-left is left semi-transparent. And, ideally that image wouldn’t be an actual SVG, since that would land us back into the ugly data URI mess we were in before.
At this point you might be thinking: “Hey, just embed the SVG in the CSS directly, define a mask in it, then point the CSS at the mask ID in the SVG!”
Nice idea! And it’s certainly doable, if you can edit the HTML. For my specific project, however, I was working in WordPress and I really wanted to confine my changes to pure CSS rather than injecting extra parts into the HTML. That would have been a lot more work. I don’t think this is uncommon; for a presentational change like this, not having to edit the HTML is useful. We’re mostly on board with the idea of avoiding semantically worthless wrapper elements just to provide styling hooks, but I feel that also applies to adding entire SVG markup to the document… or even a WordPress template.
We can use a CSS linear gradient to create a triangle shape instead:
.el {
linear-gradient(
to bottom right,
white,
white 50%,
transparent 50%,
transparent
);
}
Here that is on a radial background, so you can see it’s really transparent:
Great! We can just use that as a mask-image
on our banner, right? We need to set mask-size
, which is like background-size
, and mask-repeat
, like background-repeat
, and we’re good?
Unfortunately, no. Not so good.
The first reason is that, unless you’re using Firefox, you’ll likely see no masking at all on that example above. This is because Blink and WebKit still only support masking with a vendor prefix at the time of writing. That means we need -webkit-
prefixed versions of everything.
Vendor prefixing aside, what we’re doing is also conceptually wrong. If we confine the mask to just the bottom stripe of the image with mask-size
, then the rest of the image has no mask-image
at all, which masks it out entirely. As a result, we can’t use the sawtooth alone as a mask. What we need is a mask-image
that is a rectangle the size of the image with just a sawtooth at the bottom.
Something like this:
We do that with two gradient images. The first image is the same sawtooth triangle as above, which is set to repeat-x
and positioned at the bottom
so that it repeats only along the bottom edge of the image. The second image is another gradient that is transparent for the bottom 30px (so as to not interfere with the sawtooth), opaque above that (which is shown going from black to white in the demo), and takes up the whole size of the element.
So we now have this wedge-shaped piece, with a single triangle sawtooth at the bottom, and it occupies the entire height of our banner image in two separate pieces. Finally, we can use these pieces with mask-image
by repeating them horizontally across our image, and it should have the effect we want:
And there we have it!