tl;dr

  • clip-path is awesome for quickly changing the colors of simple graphics.
  • clip-path doesn’t work on all browsers.
  • clip-path using an SVG as the mask doesn’t work on most browsers.
  • Any SVG you want to use as a clip-path needs to be built from shapes, not <line>s.

The Problem

I’m not good with color schemes. I can pick out things on Kuler that I think might look good, but when it comes time to implement them, I have no idea what to put where or how to arrange them.

When I’m sketching out a site like this, SASS is a godsend because introducing variables into CSS lets me change an entire color scheme by changing only a few lines of code, so I can plow through a whole bunch of different ideas in mere minutes.

Certain things in this design, however - namely Flying Umbrella Dude Man Guy on the lefthand side of the main menu and the hamburger button on the right (on mobile) - do not lend themselves well to this approach. I noticed that Bootstrap’s standard hamburger menu button is just an SVG, and that got me thinking: is it possible to use SASS variables to change the stroke-color of an SVG path? Would <path stroke="$theme-primary-color;"> [...] work?

Nope. It didn’t. It might just be that I’m doing something wrong, but it didn’t work.

Introducing clip-path

When I want to separate shape and color in something like GIMP , I use a layer mask to define the shape of the object and then apply that mask to a solid color. Would the same thing be possible in CSS somehow?

Turns out it actually is, and it’s called clip-path . clip-path accepts anything included in basic-shape and, luckily for me, url() references to SVG paths.


Before we go too much further, I need to point out that - at the time of writing - clip-path is only fully supported in Firefox. I didn’t find any major issues in Chrome, but IE and Edge both display the element as a solid box of color. Rendering in Safari on iOS was a complete, hopeless mess with the SVG being stretched all around the page and re-scaling itself apparently at will.


The general idea here is that I’m going to be creating a <div> with a solid background-color that I can define using a SASS variable and then using an SVG as sort of a cookie cutter to cut out part of that <div> and leave the rest of it transparent:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
<div class="hamburger-svg-icon"></div>

<div style="max-height: 0px;">
	<svg height="0" width="0" viewBox="0 0 32 32">
		<defs>
			<clipPath id="hamburger-clip-path" clipPathUnits="objectBoundingBox" transform="scale(0.03125 0.03125)">
				[...]
			</clipPath>
		</defs>
	</svg>
</div>

<style>
	.hamburger-svg-icon {
		-webkit-clip-path: url(#hamburger-clip-path);
		clip-path: url(#hamburger-clip-path);
		background-color: $link-color;
		width: 32px;
		height: 32px;
	}
</style>

I already had a SVG version of Flying Umbrella Dude Man Guy ready to go, it was just a matter of cleaning out all the extra crap that Inkscape adds in order to get the filesize down as much as possible.

For some reason, even if height="0" and width="0" are both specified in the SVG tag, the SVG will still have some height applied to it, and will take up space on the page. For me, this space is 24px tall, and I can’t figure out where that value comes from. Setting display: none; on the SVG will cause it to appear invisible to clip-path as well, rendering that approach useless. To solve this, I stuck each SVG I would be pulling paths from into a <div> with max-height: 0px; applied to it in order to hide them from the user.

In order for the SVG to scale correctly, we’ll need to add a few attributes to the clipPath, as described by Eric Meyer . The renderer is expecting all points in the SVG to be somewhere between (0,0) and (1,1). Since this is generally never going to be the case unless you’re building your SVG from scratch with this requirement in mind, we need to scale the image down to fit into that unit square.

We do this by first adding the clipPathUnits="objectBoundingBox" attribute to the <clipPath> tag.

After that, make a note of the coordinates in the viewBox attribute all the way up in the root <svg> tag. These coords are in the sequence xstart ystart xend yend. Back in the <clipPath>, add transform="scale(x y)" after clipPathUnits and assign x and y the following values: x = 1/viewBoxx, y = 1/viewBoxy.

For instance, my Flying Umbrella Dude Man Guy SVG tag is <svg height="0" width="0" viewBox="0 0 63.5 63.5">, so the scale factor for both x and y would be 1 / 63.5 = 0.0157480, and the resulting clipPath tag looks like <clipPath clipPathUnits="objectBoundingBox" transform="scale(0.0157480 0.0157480)">.

After that’s all done, it’s a simple matter of assigning an id to each <clipPath> you’ll be using, and referencing that id in your clip-path statement.

Hand-rolling SVGs

I didn’t want to jump back into Inkscape just for my hamburger menu’s icon, so I decided I would be clever and create it by hand. This turned out to take a whole lot more time and energy than I thought it would.

I figured that since the icon is just three lines, I could use just three <line>s, but no matter what combination of positions or coordinates or strokes or stroke-widths I used, nothing showed up.

Frustrated, I tried three <path>s instead, with equally invisible results.

It took a while, but I eventually tried making a sort of hockey stick shaped path, and then added z to the end, telling the renderer to connect the last specified point to the first specified point. I got a triangle.

Now I understood something that I probably should have gotten from the beginning: clip-path requires a path with a nonzero area as an input.

Thinking of it as a cookie cutter is a really good analogy: if you have a straight line for a cookie cutter, it doesn’t matter how you jam that straight line into the dough, you’re not going to get a cookie out of it because your cutter is one-dimensional. Even with two non-parallel lines sharing some vertex, you’re still not going to get a cookie. It’s only when you have points joined in such a way that they form a two-dimensional shape with some area >0 that you’ll get a cookie.

For my hamburger menu, I ended up using three <rect>s with an ry specified so that their ends would be a little rounded. Visually, this is identical to using a <line> with stroke-linecap="round".

<svg height="0" width="0" viewBox="0 0 32 32">
    <defs>
        <clipPath id="hamburger-clip-path" clipPathUnits="objectBoundingBox" transform="scale(0.03125 0.03125)">
            <rect width="22" height="1.8" x="5" y="8.1" ry="0.9" />
            <rect width="22" height="1.8" x="5" y="15.1" ry="0.9" />
            <rect width="22" height="1.8" x="5" y="22.1" ry="0.9" />
        </clipPath>
    </defs>
</svg>

Scrapping It

As mentioned earlier, clip-path isn’t very widely supported yet.

On mobile Firefox, clip-path works excatly how you would expect it to:

clip-path working correctly on Firefox mobile

This is what it should look like.

However, on any iOS browser (Safari is shown here)…

clip-path working incorrectly on Safari mobile

Hamburger menu closed.

clip-path working incorrectly on Safari mobile

Hamburger menu open.

…the results are less than impressive. It’s not apparent from just screenshots, but the icon SVG does stretch vertically with the menu as it expands.

To be clear, this isn’t a problem with how these browsers handle clip-path itself, but the combination of an SVG and clip-path.

Because of the strange Safari behavior and the fact that both IE and Edge render the icon as just a solid block of color, I decided I had to scrap the idea for now. I’d love to be able to come back to this in the future because of how elegant of a solution it is, but too many browsers just aren’t ready yet.