Building A Circular Navigation with CSS Clip Paths

The CSS clip-path property is one of the most underused and yet most interesting properties in CSS. It can be used in conjunction with CSS Shapes to create interesting layouts, and can be taken to the extreme to create some incredibly impressive layouts and animations like the Species in Pieces project.

While exploring the creation of arbitrarily-shaped UI components using CSS and SVG, it occurred to me that the clip-path property, when combined with SVG paths, can be used to create circular menus fairly easily, especially considering the (expected) browser behaviour when handling pointer events on clipped regions as per the specification. Let's explore this idea a little further.

Some Background

A couple of years ago, I wrote an article for Codrops about using CSS transforms and some CSS trickery to create CSS-only circular menus. That technique is anything but optimal or intuitive, and requires a lot of workarounds and hacks to achieve the desired effect.

To create a menu using that technique, you fake a sector shape by skewing the menu items, and then clipping them, so to speak, by hiding the overflow on their container. And since you start out by skewing the menu items, you need to follow up by "unskewing" the content inside of them which would be distorted after their container has been skewed.

The resulting menu is not flexible, requires a lot of hacking, and the content inside of it is limited to only icons in most cases because other content would be hard to position and style inside of the skewed items. You can learn all the details of that technique in the article.

Today, the CSS clip-path property—combined with SVG paths—can also be used to create circular menus in CSS. The technique is not hacky, does not require any weird transformation workarounds, and works just as you'd expect. There are a couple of limitations and a browser bug at the time of writing of this article (see following section), but the code required to create the menu is surprisingly short, clean and easy to understand.

But before we dig into the code, and even though the code is straightforward and easy to follow, you might want to learn a little more about clipping paths first, if you're not already familiar with what they are, what they do, and how they work. You can learn all about them in an article on my blog.

We also need to go over some notes regarding browser support before we get into the code and to the live demos:

Browser Support (and Bugs)

  • We'll be using the CSS clip-path property, so first thing to note is the browser support. As the support table from CanIUse shows below, the property's support is not at its best, especially with no version of IE supporting it, not even MS Edge.
  • IE does not support clip paths in CSS yet, but is it currently "under consideration".
  • If you use a CSS basic shape function to define a clipping path, Firefox will not apply that clip path because it currently only supports clip path values that reference an SVG <clipPath> element. Firefox does not support the basic CSS shape functions yet. (See end of this section for a fallback tip.)
  • WebKit/Blink-based browsers don't handle pointer events correctly when the clip path used is defined in SVG. This is a bug. By default, pointer events should not be dispatched outside the visible area of the clipped element; that is the expected behaviour as per the specification. When you use a CSS basic shape function to define the clip path, these browsers handle pointer events correctly. However, when you apply an SVG clipPath via the clip-path property, pointer events are still dispatched outside the visible area, which will interfere with and block pointer events on any element lying behind/underneath the clipped element. I filed a bug report about this while working on this article. Let's hope it gets fixed soon. This bug means that the demo in this article is, for now, not working in WebKit/Blink-based browsers. (Sorry.)
  • There is also another bug in Blink-based browsers that causes an extremely weird rendering problem (it's a compositing issue) which also makes applying transformation effects to a clipped elements on hover practically impossible today. So, for the menu in our article, scaling the menu up and down on click, for example, using CSS scale transformations causes a huge problem, so we will be skipping the opening/closing effect. I also filed a bug report for this issue.

In short: the demo for this article currently only works as expected in Firefox at the time of publication.

Should you decide to change the clipping path used in the article and replace it with a CSS basic shape, the demo will work in other browsers (excluding IE/Edge), but not in Firefox.

If you do want to use a basic CSS shape function to define a clip path and want it to work in Firefox, you can always create the same shape using an SVG <clipPath> element and apply it as a fallback for Firefox.

For example:

.element {
  clip-path: url(#SVGPolygonShape); /* For Firefox */
  clip-path: polygon(...); /* For other browsers */
}

The last thing to note here is that only Firefox currently supports referencing a clipPath element defined in an external SVG; all other browsers require the defined SVG path to be inlined in the document. There is thread to track this issue on the Chromium Project.

Now that browser bugs and support are clear, let's dig into code. We'll start with the markup for the menu.

Marking It Up

The markup is pretty straightforward: the menu is an unordered list of items wrapped in links and containing some sort of content like text or an icon. I am using a simple "icon" label for demonstration purposes.

<ul class="menu">
  <li class="one">
    <a href="#">
      <span class="icon">icon-1</span>
    </a>
  </li>
  <li class="two">
    <a href="#">
      <span class="icon">icon-2</span>
    </a>
  </li>
  <li class="three">
    <a href="#">
      <span class="icon">icon-3</span>
    </a>
  </li>
  <li class="four">
    <a href="#">
      <span class="icon">icon-4</span>
    </a>
  </li>
  <li class="five">
    <a href="#">
      <span class="icon">icon-5</span>
    </a>
  </li>
  <li class="six">
    <a href="#">
      <span class="icon">icon-6</span>
    </a>
  </li>
</ul>

That's all the markup we need.

Defining the Clip Path (The Sector Shape)

To create a sector shape, we need to be able to draw an arc from one point to the other in the middle of a path, and that's currently not possible in CSS using the CSS basic shape functions: circle(), ellipse(), inset() or polygon(). (That is, unless you want to use the polygon() function with a very large number of points positioned so close to each other that they appear as an arc. But who would want to do that?)

So, we need a more straightforward way to draw the sector shape. And fortunately, the clip-path property grants us the ability to do that by accepting a reference to an SVG path as a value for the clipping path.

In other words, you can define the clipping path shape you want (it can even be made up of multiple separated paths) in SVG, wrap that shape in an SVG <clipPath> element with an ID, and then reference that path in CSS using the URL syntax:

clip-path: url(#clipPathID);

Thus, defining our sector shape in SVG becomes very simple. However, there is some math and positioning considerations to be taken into account.

First, you need to determine the number of items you want the menu to have; that will determine the value of the central angle for the sectors.

Next, you need to draw the sector shape using an SVG <path> element. The path commands available in SVG enable you to draw the path following some simple drawing rules.

We'll be using four path commands to draw the sector shape: M, l (small L), A and z. But before we start drawing, we need to determine where exactly the sector will clip the items, and for that, we need to plan the concept out before we get to execution.

Determining the position of the clip path on the menu items

The menu items will need to be positioned absolutely on top of each other and then clipped to sector shapes that will all form the overall circular shape of the menu.

In order to do that, we need to think of these items as a set of layers (which they literally are), and then cut off parts of these layers such that, in the end, only the sector shapes of these layers will be visible. The following image shows the layers for four of the six items of the menu we are creating:

The translucent black boxes are there only for demonstration purposes. What's really happening is the items are rotated, not the clip paths. That is, for each item (rectangular box), the item is clipped to the exact same sector shape (such that all items are identical), then each item is rotated by the necessary angle so that their clipped regions do not overlap anymore, and form the circular menu instead. The following image demonstrates that more clearly:

If you look at the second sector in the above image (try to tilt your head a bit so that the black border is on top) (Yes, I tilted my head as I worked on these illustrations), you will be able to see the sector position inside of the item more clearly: It is the exact same position as the sector in the first item, except that the first item was not rotated after it had been clipped.

Note that in the second image above, the black boxes indeed represent the list items as they would be positioned on the page, because the clip path will affect the visible/painted area of the element, but it is practically still a rectangle, even if only a small part of it is showing through its new non-rectangular viewport.

Before we move forward, here is an animated version showing how the items would be positioned, clipped and rotated to achieve the overall menu using the clipped elements. The aim of this GIF is to show that they are all clipped identically before they are rotated.

Even though the elements are clipped, they are still rectangular in principle. So when you visualise their rotation, remember that it's a rectangular element being rotated, but only the non-rectangular shape inside of it is visible.

Determining the point coordinates of the clip path when applied on the menu items

Now that we know how the items will be rotated, we know that the initial sector shape will be identical for all the items. So all we need next is to draw the sector shape in SVG. And to do that, we need to see how the coordinates of the points defining the sector will be determined.

Let's take a look at the code for the sector shape before we dissect it; the following <svg> goes into the page so that the clip path can be referenced in the CSS:

<svg height="0" width="0">
  <defs>
    <clipPath clipPathUnits="objectBoundingBox" id="sector">
      <path fill="none" stroke="#111" stroke-width="1" class="sector" d="M0.5,0.5 l0.5,0 A0.5,0.5 0 0,0 0.75,.066987298 z"></path>
    </clipPath>
  </defs>
</svg>

The most important part in the above code is the clipPathUnits="objectBoundingBox" declaration. The clipPathUnits attribute accepts two values: userSpaceOnUse and objectBoundingBox. The former will draw the path using the entire page's coordinate system, and the latter will use the item's coordinate system—which is what we want.

When you use objectBoundingBox, the coordinates of the points used to draw the sector path are set using relative values in the range [0, 1]. These values are very similar in principle to percentage values, and will be calculated relative to the element's bounding box; i.e. the element's width and height, in our case. That was is why the values in the above snippet are all less than 1.

If you use the userSpaceOnUse value instead of objectBoundingBox, the browser will use the coordinate system on the page (in the case of HTML) or the current user coordinate system in use (that of the canvas, in the case of SVG) when positioning your clip path, meaning that it may or may not be really applied to your element, depending on where the element is on the page or canvas. You can read a little more about this in my article on the subject.

Knowing how the sector will be positioned inside the menu item (square) from the previous section, we can illustrate the position and relative point coordinates:

These coordinates, along with the relative circle radius for the sector, will be used by the path commands to draw the sector shape.

Given how we want it to be positioned on our menu items, the coordinates of the points defining the clip path can be determined as shown in the image. Three points are required to draw the sector shape, and we also need the radius of the circle for that sector.

The radius of the circle is 0.5, which is half the width of the element. The center of that circle is also on (0.5, 0.5). The second point on the sector making up the base is located at (1, 0.5). The third point coordinates, by way of some simple math, is located at (0.75, 0.066987298). Then, using SVG arc (A) and line commands (l), the path is drawn. We won't get into the details of how it is drawn because it's outside the scope of this tutorial, but here is an interactive demo showing this drawing in action. Click on the button to play it.

See the Pen Building A Circular Menu With SVG — #1 by Sara Soueidan (@SaraSoueidan) on CodePen.

To learn how the path commands are used to draw the sector shape, refer to this article on my blog.

With this data at hand, the <path> defining our <clipPath> can be created, and is ready to be applied to our menu items.

Clipping The Menu Items

With the SVG clip path ready, we can now clip the menu items using the clip-path property.

First, the items are positioned absolutely on top of each other inside the menu. We start by creating a positioning context:

.menu {
  position: relative;

  list-style: none;
  margin: 30px auto;

  /* padding trick for maintaining aspect ratio */
  height: 0;
  padding: 0;
  padding-top: 70%;
  width: 70%;
}

To make sure the menu has square dimensions, I'm using the padding hack to make sure it maintains a 1:1 aspect ratio. Since we want it to be fluid, I'm using percentage values for the width. Using media queries, you can specify the minimum and maximum dimensions you want, and adjust the padding (for the hack) accordingly. The live demo below includes that part, so you can play with these values and adjust them to your liking.

Next, position the items inside the menu and clip them to the sector shape:

.menu li { 
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;

  clip-path: url(#sector)
}

.menu li a {
  display: block;
  width: 100%;
  height: 100%;
}

.menu li:hover {
  background-color: gold;
}

This will clip all items to the sector shape, and will only result in one item being shown (the last one, on top of all the others), with the rest "behind" it. The remaining items are not visible because they are all clipped in the same area and are thus covered by the last one.

To show the rest of the items and form our circular menu, we need to rotate the items by the necessary angles. Item #1, which has index 0, will be rotated by 0 * angle = 0 * 60 = 0deg.

Item #2 will be rotated by 1 * 60 = 60deg; item #3 will be rotated by 2 * 60 = 120deg. And so on.

.one {
  background-color: $base;
  transform: rotate(0deg);
}
.two {
  background-color: darken($base, 7%);
  transform: rotate(-60deg);
}
.three {
  background-color: darken($base, 14%);
  transform: rotate(-120deg);
}
.four {
  background-color: darken($base, 21%);
  transform: rotate(-180deg);
}
.five {
  background-color: darken($base, 28%);
  transform: rotate(-240deg);
}
.six {
  background-color: darken($base, 35%);
  transform: rotate(-300deg);
}

We could have used a Sass loop to automate this, but let's lets not get too far away from our focus.

And that's all it takes to get the circular menu items using CSS clip paths. Here is the live demo:

See the Pen Circular Menu using CSS clip-path by Sara Soueidan (@SaraSoueidan) on CodePen.

Note that, since Chrome has that bug with pointer events at the time of writing of this article, you can replace the SVG clip path reference with this:

clip-path: polygon(50% 50%, 100% 50%, 75% 6.6%);

to get an idea of how the menu works with proper pointer events. Hover over the items to see their background color change. Remember, the new clip path will not work in Firefox, though. So, for the time being, you can add the above line after the clip-path URL syntax, so that Firefox can use the latter.

This is how the menu will look like with the polygon() clip path applied:

I've added this line of CSS to the above menu so you can uncomment it if you want.

Adding Icons/Content To The Menu Items

... is simple. All you need to do is make sure the icons/text/whatever are visible inside the visible area of the element (the sector shape). Again, using the coodrdinate system established by the element's height and width, you can estimate the position of the items so that they show up inside the new sector-shaped viewport.

It might take some experimenting to get the exact alignment that you want, depending on the content of the items.

For our demo, I am using text, so positioning that text at 30% down the element and 15% to the left of the right edge was a good place to start.

After we've specified the position for the text in our example, we need to rotate the text by the same angle as the sector to make sure it looks as it should inside the menu, otherwise you would end up with this:

instead of this:

And the styles for the text inside the items are:

.icon {
  position: absolute;
  /* exact values here depend on what you are placing inside the items (icon, image, text, etc.) */
  right: 15%;
  top: 30%;
  /* angle of rotation = angle of the sector itself */
  transform: rotate(60deg);

  /* style further as needed */
}

Another Way

The above technique is the simplest way to achieve this circular menu using CSS clip paths.

Instead of using the same sector shape to clip all elements and then rotate those elements in CSS, you could have avoided rotating the elements and used a rotated sector shape instead, so that each menu item will be clipped such that the resulting sectors are already rotated to form the circular shape.

However, that technique is more complicated because you need to specify the point coordinates as well as rotation for every SVG <path>, not to mention it would require more work to get the position for the content of each item to show through each of those sectors. It is more complicated and time-consuming, so definitely not better than the above technique.

That said, I still want to show you what the code for the clip paths in this case would look like:

<svg height="0" width="0">
  <defs>
    <clipPath clipPathUnits="userSpaceOnUse" transform="matrix(1,0,0,1,0,0)" id="one-2">
      <path fill="none" stroke="#111" stroke-width="1" class="sector" d="M250,250 l250,0 A250,250 0 0,0 375,33.49364905389035 z"></path>
    </clipPath>
    <clipPath clipPathUnits="userSpaceOnUse" transform="matrix(0.5,-0.86602,0.86602,0.5,-91.5063509461097,341.5063509461096)" id="two-2">
      <path fill="none" stroke="#111" stroke-width="1" class="sector" d="M250,250 l250,0 A250,250 0 0,0 375,33.49364905389035 z"></path>
    </clipPath>
    <clipPath clipPathUnits="userSpaceOnUse" transform="matrix(-0.49999,-0.86602,0.86602,-0.49999,158.49364905389024,591.5063509461097)" id="three-2">
      <path fill="none" stroke="#111" stroke-width="1" class="sector" d="M250,250 l250,0 A250,250 0 0,0 375,33.49364905389035 z"></path>
    </clipPath>
    <clipPath clipPathUnits="userSpaceOnUse" transform="matrix(-1,0,0,-1,500,500)" id="four-2">
      <path fill="none" stroke="#111" stroke-width="1" class="sector" d="M250,250 l250,0 A250,250 0 0,0 375,33.49364905389035 z"></path>
    </clipPath>
    <clipPath clipPathUnits="userSpaceOnUse" transform="matrix(-0.5,0.86602,-0.86602,-0.5,591.5063509461097,158.4936490538905)" id="five-2">
      <path fill="none" stroke="#111" stroke-width="1" class="sector" d="M250,250 l250,0 A250,250 0 0,0 375,33.49364905389035 z"></path>
    </clipPath>
    <clipPath clipPathUnits="userSpaceOnUse" transform="matrix(0.5,0.86602,-0.86602,0.5,341.5063509461096,-91.5063509461097)" id="six-2">
      <path fill="none" stroke="#111" stroke-width="1" class="sector" d="M250,250 l250,0 A250,250 0 0,0 375,33.49364905389035 z"></path>
    </clipPath>
  </defs>
</svg>

That's quite a lot of SVG code compared to the couple of lines from the previous sections.

The above snippet contains a clip path for each sector, rotated as necessary, and without the coordinates converted into relative ones in the range [0, 1]; you would need to convert those coordinates as well. Each of the above sectors can then be used as a clip path for its respective element in the menu. Not very optimal this one, is it?

Note that the above code is generated from a circular menu generator I created a while back, so, no, I did not code this by hand. (More about it in the next section.)

SVG Circular Menus

The clip-path technique is a great one and, conceptually, it works pretty well; but the current browser bugs make the resulting menu practically unusable today.

A few months ago I explored the idea of using SVG and SVG paths to create circular menus, considering how well-equipped SVG is for creating non-rectangular shapes in general. Using SVG paths, the sector shapes would be drawn and wrapped in a link <a>. Creating the sectors is straightforward and requires the same considerations and steps as we did earlier, except that you can stick to absolute values for the point coordinates instead of relative values, because the entire SVG canvas will be the coordinate system where the sectors are drawn. You don't need to apply the sector shapes to any element—the shape paths themselves will be the menu items.

Thus, if you do need a circular menu to use in your UIs today, then SVG is currently your best option. Support is great (IE9+ and all modern browsers), and it works just as you'd expect. Plus, you'd get the flexibility of SVG, especially that you get to use SVG icons inside the menu instead of icon fonts. Should you be interested, you can check out the article explaining how to do that here.

Since it takes quite some time to create a circular menu in SVG, I've also created a generator that enables you to customise the defining characteristics of the menu visually, and then generates the code for that menu for you, ready to be embedded. The tool also includes an extensive guide about the generated code, how to embed it, how to animate the menu opening/closing, and even some UX considerations for when you want to use circular menus.

Circulus: the SVG ciruclar menu generator.

You can find the generator here.

Final Words

Don't use the old CSS transforms technique for creating circular menus. Clip paths in CSS are pretty awesome and very powerful, but until support gets better, they're not always a go-go. SVG is also just as awesome, and is currently better suited for creating circular menus, at least until CSS clip path support gets better.

I hope you liked this article and found it useful. Thanks for reading!