CSS: Flexible Repeating SVG Masks

This is a technique I used recently to create a flexible, repeating pattern using SVG with CSS mask. There are live examples in this post and they’re available as a CodePen.

The need came from something I designed at work. I gave the bottom of the header on each page a little squiggle to make it a bit more interesting than a straight line. Along with that, I designed a color system for each type of page. Common pages parade in purple, company content is true blue, blog posts read in red, and contests own orange. With StreetCred I’ve been throwing a rainbow of bright colors to see what’s fun and what sticks.

A screenshot of a purple, blue, red, and orange banner from the StreetCred website.
fig 1: Banner squiggles from streetcred.co

Along with the current colors, we’re getting ready to ship new features that use even more colors. We’ll make pages for those and they’ll need themed banner squiggles. We need a flexible way to do that.

How Should This Work?

For the design, I needed a seamless, horizontally repeating pattern. I needed SVG so it was crisp at any size. I wanted a single SVG image that I could color using CSS. Because we could have n-number of colors, creating a new SVG for every color wasn’t a future-facing option. The squiggly banner using this approach is live on streetcred.co. Let’s look at an isolated demo to see how the pieces fit together.

Creating the Demo SVG

First, I made a quick doodle of a squiggly line in Sketch using the vector tool. The important part was to make sure the line was seamless when repeated horizontally. I did that by making sure the first point and last point were at the same location on the y-axis. And that the bezier curves leading to them was flat when it reached the edge of the artboard. This is a simple shape, so it was easy to eyeball. A more complex design would involve more work.

A screenshot in Sketch app showing the demo squiggle vector being created.
fig 2: Creating the demo SVG in Sketch

To test if the shape is seamless I just duplicated the artboard and dragged it into place. Nothing fancy again, a quick eyeball is enough for this.

When I create graphics for SVG use, I simplify them as much as possible. For this, I expanded the stroke. That way instead of exporting a path with a stroke, I end up with a path with a fill. In my experience, filled shapes are more flexible when you’re working with them in browsers. Helps avoid scaling issues with strokes. SVG prep is a deep topic that I plan to write more about in the future.

Exporting SVG from Sketch leaves a lot of crud in that we don’t need. My next stop is SVGOMG. After optimizing we end up with an SVG with a single path element. I truncated the value of the d attribute for brevity.

<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 250 75">
  <path id="squiggle" d="M12 19.778C12 1.318..." />
</svg>

We’ll use the id attribute as a reference in CSS. Notice we don’t need presentational attributes like fill. SVG provides the shape. CSS handles the color.

The Code

The HTML is a single div. The CSS is three properties.

<div class="repeater"></div>
.repeater {
  background-color: red;
  /* This fixed height is only for demo, your use cases might not need it */
  height: 75px;
  mask-image: url("/path/to/repeater.svg#squiggle");
}

As of this writing, this CSS works in Firefox, but not in Chrome, Safari, or Edge. WebKit/Blink browsers still require vendor-prefixed mask properties. We don’t want to have to repeat the path to the image. Let’s store it in a custom property to make it reusable.

.repeater {
  background-color: red;
  /* This fixed height is only for demo, your use cases might not need it */
  height: 75px;
  --svg: url("/path/to/repeater.svg#squiggle");
  -webkit-mask-image: var(--svg);
  mask-image: var(--svg);
}

With the prefixed version of mask-image in place, this works in all current versions of Chrome, Firefox, Safari, and Edge.

fig 3: A live demo of the technique. The CSS is inline for inspection.

What did we do?

mask-image is doing the heavy lifting here. We’re giving it a reference to an external SVG file and the id attribute of the path we want. Mask is hiding anything in our div that doesn’t intersect with that shape. So we see the background-color only where the path is.

mask is like CSS bacground. It has a mask-repeat property that defaults to repeat. Check the MDN docs for more that.

What else?

With a functional foundation in place, we can have fun with it. We can style the underlying div any way we want to create different effects. We can change the color.

.repeater--orange {
  background-color: orange;
}
fig 4: A live demo of the technique showing that we can change the color with CSS.

We can take that further by setting a background image.

.repeater--gradient {
  background: transparent linear-gradient(90deg, red, purple, blue, green);
}
fig 5: A live demo of the technique showing that we can use a background image

Changing the background color and image is fun, we can also change the height of the div to produce something different.

.repeater--sized {
  height: 18px;
}
fig 6: A live demo of the technique showing that we can change the size

We can change the color, bacground-image, and height. Let’s introduce the mask-size property to squish the SVG into a type of texture.

.repeater--textured {
  --texture-lines: 10;
  --mask-size: calc(250px / var(--texture-lines));
  -webkit-mask-size: var(--mask-size);
  mask-size: var(--mask-size);
}
fig 7: A live demo of the technique showing that we can use mask-size to produce a texture

Again, we’re using a custom property to not have to repeat the size value. We’re using calc here as a convenience. 250px is the height of the viewbox of the SVG. We divide that by the number of rows we want to make sure we don’t have partial rows.

We can also change the position of the mask using mask-position. And, if we can change a value with CSS, that means we can change it over time. Change over time equals motion.

@keyframes move {
  to {
    --pos: 150%;
    -webkit-mask-position: var(--pos);
    mask-position: var(--pos);
  }
}
.repeater--animated {
  animation: move 0.6s infinite linear alternate;
}
fig 8: A live demo of the technique showing that we can animate the mask position.

These are toy examples, but they show that with a few building blocks we can construct a lot of variations. With more complex SVG images and more CSS, we can use this technique to produce all sorts of fun stuff. And be future-flexible while doing it.

Thanks for reading