 I recently came across an interesting sliced disc design. The disc had a diagonal gradient and was split into horizontal slices, offset a bit from left to right. Naturally, I started to think what would the most efficient way of doing it with CSS be.

The first thought was that this should be doable with `border-radius`, right? Well, no! The thing with `border-radius` is that it creates an elliptical corner whose ends are tangent to the edges it joins.

My second thought was to use a `circle()` clipping path. Well, turns out this solution works like a charm, so let’s take a close look at it!

Note that the following demos won’t work in Edge as Edge doesn’t yet support `clip-path` on HTML elements. It could all be emulated with nested elements with `overflow: hidden` in order to have cross-browser support, but, for simplicity, we dissect the `clip-path` method in this article.

### Slicing a disc into equal parts

As far as the HTML structure goes, we generate it with a preprocessor to avoid repetition. First off, we decide upon a number of slices `n`. Then we pass this number to the CSS as a custom property `--n`. Finally, we generate the slices in a loop, passing the index of each to the CSS as another custom property `--i`.

``````- var n = 8;

style :root { --n: #{n} }

- for(var i = 0; i < n; i++) .slice(style=`--i: \${i}`)``````

Moving on to the CSS, we first decide upon a diameter `\$d` for our disc. This is the `width` of our slices. The `height` is the diameter divided by the number of items `calc(#{\$d}/var(--n))`.

In order to be able to tell them apart, we give our slices dummy backgrounds determined by parity.

``````\$d: 20em;

.slice { --parity: 0; width: \$d; height: calc(#{\$d}/var(--n)); background: hsl(36, calc(var(--parity)*100%), calc(80% - var(--parity)*30%));  &:nth-of-type(2n) { --parity: 1 }
}``````

We also position our slices in the middle with a column `flex` layout on their container (the `body` in our case).

See the Pen by thebabydino (@thebabydino) on CodePen.

To get the disc shape we use a `circle()` clipping path having the radius `\$r` equal to half the diameter `.5*\$d` and the central point dead in the middle of the assembly. Since we set this `clip-path` on the slices, the position of the central point for each slice is relative to the slice itself.

Horizontally, it’s always in the middle, at `50%` of the slice. Vertically, it needs to be in the middle of the assembly, so that’s where the total number of items and the item’s index which we’ve passed as CSS variables from the preprocessor code come into play.

In the middle of the assembly means at half the height of the assembly from the top of the assembly. Half the height of the assembly is half the diameter `.5*\$d`, which is equivalent to the radius `\$r`. But this value is relative to the whole assembly and we need one that’s relative to the current slice. In order to get this, we subtract the vertical position of the current slice relative to the assembly, that is, how far the top of the current slice is relative to the top of the assembly.

The first slice (of index `--i: 0`) is at the very top of the assembly, so the amount we subtract in this case is `0`.

The second slice (of index `--i: 1`) is at one slice height from the top of the assembly (the space occupied by the first slice), so the amount we subtract in this case is `1` slice heights.

The third slice (of index `--i: 2`) is at two slice heights from the top of the assembly (the space occupied by the first and second slices), so the amount we subtract in this case is `2` slice heights.

In the general case, the amount we subtract for each slice is the slice’s index (`--i`) multiplied by one slice `height`.

``````--h: calc(#{d}/var(--n)); /* slice height */
clip-path: circle(\$r at 50% calc(#{\$r} - var(--i)*var(--h))``````

See the Pen by thebabydino (@thebabydino) on CodePen.

After doing this, we can offset the slices based on parity.

``````--sign: calc(1 - 2*var(--parity));
transform: translate(calc(var(--sign)*2%))``````

We now have our sliced disc!

See the Pen by thebabydino (@thebabydino) on CodePen.

### Spacing out the slices

The first thought that comes to mind here is to use a `margin` on each slice.

See the Pen by thebabydino (@thebabydino) on CodePen.

This may be a good result in some cases, but what if we don’t want our disc to get elongated?

Well, we have the option of limiting the `background` to the `content-box` and adding a vertical `padding`:

``````box-sizing: border-box;
background: hsl(36, calc(var(--parity)*100%), calc(80% - var(--parity)*30%))  content-box;``````

Of course, in this case, we need to make sure `box-sizing` is set to `border-box` so that the vertical `padding` doesn’t add to the `height`.

See the Pen by thebabydino (@thebabydino) on CodePen.

The one little problem in this case is that it also cuts off the top of the first slice and the bottom of the last slice. This may not be an issue in some cases and we can always reset the `padding-top` on the `:first-of-type` and the `padding-bottom` on the `:last-of-type` to `0`:

``````.slice { /* other styles */ padding: .125em 0;  &:first-of-type { padding-top: 0 } &:last-of-type { padding-bottom: 0 }
}``````

However, we also have a one-line solution to this problem of creating gaps in between the slices: add a `mask` on the container!

This `mask` is a `repeating-linear-gradient()` which creates transparent stripes of the thickness of the gap `\$g`, repeats itself after a slice `height` and is limited to the disc diameter `\$d` horizontally and to the disc diameter `\$d` minus a gap `\$g` vertically (so that we don’t mask out the very top and the very bottom as we also did initially with the `padding` approach).

``mask: repeating-linear-gradient(red 0, red calc(var(--h) - #{\$g}),  transparent 0, transparent var(--h))  50% calc(50% - #{.5*\$g})/ #{\$d} calc(#{\$d} - #{\$g})``

Note that in this case we need to set the slice `height` variable `--h` on the container as we’re using it for the `mask`.

See the Pen by thebabydino (@thebabydino) on CodePen.

### Continuous `background`

In order to have a continuous gradient `background`, we need to give this `background` a height equal to that of the disc and set its vertical position relative to each slice such that it always starts from the top of the assembly… wherever that may be located relative to the slice.

The top of the first slice (of index `--i: 0`) coincides with that of the assembly, so our background starts from `0` vertically.

The top of the second slice (of index `--i: 1`) is `1` slice height below that of the assembly, so its `background` starts from `1` slice `height` above vertically. Since the positive direction of the y axis is down, this means our `background-position` along the y axis is `calc(-1*var(--h))` in this case.

The top of the third slice (of index `--i: 2`) is `2` slice heights below that of the assembly, so its `background` starts from `2` slice heights above vertically. This makes our `background-position` along the y axis is `calc(-2*var(--h))`.

We notice a pattern here: in general, the `background-position` along the y axis for a slice is `calc(-1*var(--i)*var(--h))`.

``````background:  linear-gradient(#eccc05, #c26e4c, #a63959, #4e2255, #333f3d)  /* background-position */ 50% calc(-1*var(--i)*var(--h))/
100% \$d /* background-size */``````

See the Pen by thebabydino (@thebabydino) on CodePen.

But if we want a left to right gradient, then our background isn’t continuous anymore, something that becomes really obvious if we tweak the stop positions a bit in order to have abrupt changes:

``````background: linear-gradient(90deg,  #eccc05 33%, #c26e4c 0, #a63959 67%, #4e2255 0, #333f3d)   /* background-position */ 50% calc(-1*var(--i)*var(--h))/
100% \$d /* background-size */``````

See the Pen by thebabydino (@thebabydino) on CodePen.

In order to fix this issue, we set the offset as a Sass variable `\$o`, set the horizontal `background-size` to the slice `width` (`100%` or `\$d`) plus twice the offset and make sure we attach the `background` for the slices that move to the left (in the negative direction of the x axis, so by `-\$o`) on the left side of the slice (`background-position` along the x axis is `0%`) and for the slices that move to the right (in the positive direction of the x axis, so by `\$o`) on the right side of the slice (`background-position` along the x axis is `100%`).

``````\$o: 2%;
transform: translate(calc(var(--sign)*#{\$o}));
background: linear-gradient(90deg,  #eccc05 33%, #c26e4c 0, #a63959 67%, #4e2255 0, #333f3d)   /* background-position */ calc((1 - var(--parity))*100%) calc(-1*var(--i)*var(--h))/
calc(100% + #{2*\$o}) \$d /* background-size */``````

See the Pen by thebabydino (@thebabydino) on CodePen.

This works for gradients at any angle, as it can be seen in the interactive demo below – drag to change the gradient angle:

See the Pen by thebabydino (@thebabydino) on CodePen.

It also works for images, though in this case we need to remove the second `background-size` value so the image doesn’t get distorted, which leaves us with the caveat of getting vertical repetition if the image’s aspect ratio is greater than `calc(#{\$d} + #{2*\$o}) : #{\$d}`. This isn’t the case for the square image we’re using below, but it’s still something to keep in mind.

See the Pen by thebabydino (@thebabydino) on CodePen.

Another thing to note is that above, the top of the image is attached to the top of of the assembly. If we want the middle of the image to be attached to the middle of the assembly, we need to tweak the vertical component of the `background-position` a bit.

First off, to attach the middle of the image to the middle of a slice, we use a `background-position` value of `50%`. But we don’t want the middle of the image in the middle of each slice, we want it in the middle of the assembly for all slices. We already know the distance from the top of each slice to the vertical midpoint of the whole assembly – it’s the y coordinate of the clipping circle’s central point:

``````--y: calc(#{\$r} - var(--i)*var(--h));
clip-path: circle(\$r at 50% var(--y))``````

The distance from the vertical midpoint of each slice to that of the assembly is this value `--y` minus half a slice’s `height`. So it results that the `background-position` we need along the y axis in order to have the vertical midpoint of the image attached to that of the assembly is `calc(50% + var(--y) - .5*var(--h))`.

See the Pen by thebabydino (@thebabydino) on CodePen.

### Incremental slices

This means our slices don’t have the same `height` anymore. For example, the first one could have a unit `height`, the second one twice this `height`, the third one three times this `height` and so on…

The added heights of all these slices should equal the disc diameter. In other words, we should have the following equality:

``h + 2*h + 3*h + ... + n*h = d``

This can also be written as:

``h*(1 + 2 + 3 + ... + n) = d``

which makes it easier to notice something! Within the parenthesis, we have the sum of the first `n` natural numbers, which is always `n*(n + 1)/2`!

So our equality becomes:

``h*n*(n + 1)/2 = d``

This allows us to get the unit height `h`:

``h = 2*d/n/(n + 1)``

Applying this to our demo, we have:

``````--h: calc(#{2*\$d}/var(--n)/(var(--n) + 1));
height: calc((var(--i) + 1)*var(--h));``````

See the Pen by thebabydino (@thebabydino) on CodePen.

Just like in the case of equal slices, the y coordinate of the central point of the clipping `circle()` is the disc radius `\$r` minus the distance from the top of the assembly to the top of the current slice. This is the sum of the heights of all previous slices.

In the case of the first slice (`--i: 0`), we have no previous slice, so this sum is `0`.

In the case of the second slice (`--i: 1`), we only have the first slice before and its height is the unit height (`--h`).

In the case of the third slice (`--i: 2`), the sum we want is that between the `height` of the first slice, which equals the unit height and that of the second slice, which is twice the unit height. That’s `calc(var(--h) + 2*var(--h))` or `calc(var(--h)*(1 + 2))`.

In the case of the third slice (`--i: 3`), the sum is that between the `height` of the first slice, which equals the unit height, that of the second slice, which is twice the unit height and that of the third slice, which is three times the unit height. That’s `calc(var(--h) + 2*var(--h) + 3*var(--h))` or `calc(var(--h)*(1 + 2 + 3))`.

Now we can see a pattern emerging! For every slice of index `--i`, we have that the added height of its previous slices is the unit height `--h` times the sum of the first `--i` natural numbers (and the sum of the first `--i` natural numbers is `calc(var(--i)*(var(--i) + 1)/2)`). This means our `clip-path` value becomes:

``circle(\$r at 50% calc(var(--h)*var(--i)*(var(--i) + 1)/2))``

We add the offset back in and we have the following result:

See the Pen by thebabydino (@thebabydino) on CodePen.

Sadly, having incremental slices means the `repeating-linear-gradient()` mask method of creating gaps cannot work anymore. What still works however just fine is the vertical `padding` method and we can set the padding values such that the top one is `0` for the first slice and the bottom one is `0` for the last slice.

``padding:  calc(var(--i)*#{\$g}/var(--n)) /* top */ 0 /* lateral */ calc((var(--n) - 1 - var(--i))*#{\$g}/var(--n)) /* bottom */``

See the Pen by thebabydino (@thebabydino) on CodePen.

For a gradient `background`, the main idea remains the same as in the case of the equal slices. There are just two things we need to take into account.

One, the `background-position` along the y axis is minus the distance (in absolute value) between the top of the assembly and the top of the current slice. This distance isn’t `calc(var(--i)*var(--h))` like in the case of equal slices of height `--h` anymore. Instead it’s, as computed a bit earlier, `calc(var(--i)*(var(--i) + 1)/2*var(--h))`. So the `background-position` along the y axis is `calc(-1*var(--i)*(var(--i) + 1)/2*var(--h))`.

And two, we want our `background` clipped to the `content-box` so that we keep the gaps, but we need to keep the `background-origin` to its initial value of `padding-box` so that our gradient stays continuous.

``````background:  linear-gradient(var(--a),  #eccc05, #c26e4c, #a63959, #4e2255, #333f3d)
/* background-position */ calc((1 - var(--parity))*100%) /* x component */  calc(-1*var(--i)*(var(--i) + 1)/2*var(--h)) /* y component */ /
/* background-size */ calc(100% + #{2*\$o}) \$d
padding-box /* background-origin */ content-box /* background-clip */;``````

See the Pen by thebabydino (@thebabydino) on CodePen.

For an image `background` whose midpoint is attached to the middle of our assembly, we need to take into account the fact that half a slice `height` isn’t the same value for all slices anymore. Now the `height` of a slice is `calc((var(--i) + 1)*var(--h))`, so this is the value we need to subtract in the formula for the y component of the `background-position`.

``````--y: calc(#{\$r} - .5*var(--i)*(var(--i) + 1)*var(--h));
background:  url(/amur_leopard.jpg)
/* background-position */ calc((1 - var(--parity))*100%) /* x component */ calc(50% + var(--y) - .5*(var(--i) + 1)*var(--h)) /* y component */ /
/* background-size */ calc(100% + #{2*\$o})
padding-box /* background-origin */ content-box /* background-clip */;
clip-path: circle(\$r at 50% var(--y));``````

See the Pen by thebabydino (@thebabydino) on CodePen.

### Vertical slices

We can also slice our disc along the other direction. This means removing the `flex-direction: column` declaration from the container and letting the `flex-direction` be the initial one (row), switching the `width` and the `height`, the x and y coordinates of the circular clipping path’s central point, the direction along which we shift the slices, the dimensions and x and y positions of the masking gradient, which we also need to rotate so that it goes along the x axis.

``````body { /* same as before */ --w: calc(#{\$d}/var(--n)); mask: repeating-linear-gradient(90deg,  red 0, red calc(var(--w) - #{\$g}),  transparent 0, transparent var(--w))  calc(50% - #{.5*\$g}) 50% / calc(#{\$d} - #{\$g}) #{\$d}
}

.slice { /* same as before */ width: var(--w); height: \$d; transform: translatey(calc(var(--sign)*2%)); background: hsl(36, calc(var(--parity)*100%), calc(80% - var(--parity)*30%)); clip-path: circle(\$r at calc(#{\$r} - var(--i)*var(--w)) 50%)
}``````

This gives us equal vertical slices with alternating backgrounds:

See the Pen by thebabydino (@thebabydino) on CodePen.

For the gradient case, we need to also reverse the two `background` dimensions and the `background` positions along the x and y axes:

``````background:  linear-gradient(135deg,  #eccc05 15%, #c26e4c, #a63959, #4e2255, #333f3d 85%)
/* background-position */ calc(-1*var(--i)*var(--w)) calc((1 - var(--parity))*100%)/
#{\$d} calc(100% + #{2*\$o}) /* background-size */``````

See the Pen by thebabydino (@thebabydino) on CodePen.

For incremental slices, we combine the incremental case with the vertical case, which means swapping the values we have for the previous incremental case along the two axes:

``````--w: calc(#{2*\$d}/var(--n)/(var(--n) + 1));
width: calc((var(--i) + 1)*var(--w)); height: \$d;
clip-path: circle(\$r at calc(#{\$r} - .5*var(--i)*(var(--i) + 1)*var(--w)) 50%);``````

See the Pen by thebabydino (@thebabydino) on CodePen.

To create the gaps, we use the `padding` method. But since we’re now in the vertical case, we need horizontal paddings, on the left and on the right and to make sure the `padding-left` for the first slice is `0` and the `padding-right` for the last slice is also `0`:

``````box-sizing: border-box;
padding:  0 /* top */ calc((var(--n) - 1 - var(--i))*#{\$g}/var(--n)) /* right */ 0 /* bottom */ calc(var(--i)*#{\$g}/var(--n)) /* left */;
background:  hsl(36, calc(var(--parity)*100%), calc(80% - var(--parity)*30%))  content-box``````

See the Pen by thebabydino (@thebabydino) on CodePen.

Finally, we have the gradient case:

``````background:  linear-gradient(135deg,  #eccc05 15%, #c26e4c, #a63959, #4e2255, #333f3d 85%)  /* background-position */  calc(-.5*var(--i)*(var(--i) + 1)*var(--w)) /* x component */ calc((1 - var(--parity))*100%) /* y component */ /
/* background-size */ #{\$d} calc(100% + #{2*\$o})
padding-box /* background-origin */ content-box /* background-clip */;``````

See the Pen by thebabydino (@thebabydino) on CodePen.

### 2D case

Again, we generate it with a bit of Pug, the total number of items being the product between the number of columns and the number of rows. For simplicity, we keep the number of rows and the number of columns equal.

``````- var n = 8, m = Math.pow(n, 2);

style :root { --n: #{n}; --i: 0; --j: 0 } - for(var i = 1; i < n; i++) { | .tile:nth-of-type(#{n}n + #{i + 1}) { --i: #{i} } | .tile:nth-of-type(n + #{n*i + 1}) { --j: #{i} } - }
- for(var i = 0; i < m; i++) .tile``````

We’ve also passed the column and row indices (`--i` and `--j` respectively) to the CSS.

Since we’re in the 2D case, we switch from using a 1D layout (`flex`) to using a 2D one (`grid`). We also start with the disc diameter `\$d` and, given the number of columns is equal to that of rows (`--n`), our disc gets divided into identical tiles of edge length `--l: calc(#{\$d}/var(--n))`.

``````\$d: 20em;

body { --l: calc(#{\$d}/var(--n)); display: grid; place-content: center; grid-template: repeat(var(--n), var(--l))/ repeat(var(--n), var(--l))
}``````

To create the gaps in between the tiles, we use the `padding` approach on the `.tile` elements and combine the horizontal and vertical cases such that we have the `padding-top` for the first row is `0`, the `padding-left` for the first column is `0`, the `padding-bottom` for the last row is `0` and the `padding-right` for the last-column is `0`.

``padding:  calc(var(--j)*#{\$g}/var(--n)) /* top */ calc((var(--n) - 1 - var(--i))*#{\$g}/var(--n)) /* right */ calc((var(--n) - 1 - var(--j))*#{\$g}/var(--n)) /* bottom */ calc(var(--i)*#{\$g}/var(--n)) /* left */``

Note that we’ve used the row index `--j` for the top to bottom direction (vertical paddings) and the column index `--i` from the left to right direction (lateral paddings).

To get the disc shape, we again combine the horizontal and vertical cases, using the column index `--i` to get the x coordinate of the circular clipping path’s central point and the row index `--j` to get its y coordinate.

``clip-path:  circle(\$r at calc(#{\$r} - var(--i)*var(--l))  calc(#{\$r} - var(--j)*var(--l)))``

See the Pen by thebabydino (@thebabydino) on CodePen.

For a gradient `background`, it’s again combining the horizontal and the vertical cases and taking into account that here we have no offset at this point, which means the `background-size` is the disc diameter `\$d` along both axes.

``````background:  linear-gradient(135deg,  #eccc05 15%, #c26e4c, #a63959, #4e2255, #333f3d 85%)
/* background-position */ calc(-1*var(--i)*var(--l))  calc(-1*var(--j)*var(--l)) /
#{\$d} #{\$d} /* background-size */ padding-box /* background-origin */ content-box /* background-clip */``````

See the Pen by thebabydino (@thebabydino) on CodePen.

For an image `background`, we remove the second `background-size` value so we prevent the image from getting stretched if it’s not square. We also adapt the code for attaching the image’s midpoint to that of the grid from the 1D case to the 2D case:

``````--x: calc(#{\$r} - var(--i)*var(--l));
--y: calc(#{\$r} - var(--j)*var(--l));
background: url(/amur_leopard.jpg)
/* background-position */  calc(50% + var(--x) - .5*var(--l))  calc(50% + var(--y) - .5*var(--l)) /
#{\$d} /* background-size */ padding-box /* background-origin */  content-box /* background-clip */;
clip-path: circle(\$r at var(--x) var(--y))``````

See the Pen by thebabydino (@thebabydino) on CodePen.

In the incremental case, we don’t have the same dimensions for all tiles, so we use `auto` sizing for `grid-template`:

``````body { /* same as before */ grid-template: repeat(var(--n), auto)/ repeat(var(--n), auto)
}``````

Just like in the 1D case, we start by computing a unit edge length `--u`:

``--u: calc(#{2*\$d}/var(--n)/(var(--n) + 1))``

We then set incremental dimensions along both axes for our tile elements:

``````width: calc((var(--i) + 1)*var(--u));
height: calc((var(--j) + 1)*var(--u))``````

We also need to adapt the coordinates of the clipping circle’s central point to the incremental case:

``clip-path:  circle(\$r at calc(#{\$r} - .5*var(--i)*(var(--i) + 1)*var(--u))  calc(#{\$r} - .5*var(--j)*(var(--j) + 1)*var(--u)))``

See the Pen by thebabydino (@thebabydino) on CodePen.

For a gradient `background`, we adapt the equal tiles version to the incremental case. This means tweaking the `background-position` as we did before for the incremental slices, only this time we do it along both axes, not just along one:

``````background:  linear-gradient(135deg,  #eccc05 15%, #c26e4c, #a63959, #4e2255, #333f3d 85%)
/* background-position */ calc(-.5*var(--i)*(var(--i) + 1)*var(--l))  calc(-.5*var(--j)*(var(--j) + 1)*var(--l)) /
#{\$d} #{\$d} /* background-size */ padding-box /* background-origin */ content-box /* background-clip */``````

See the Pen by thebabydino (@thebabydino) on CodePen.

Finally, we have the image `background` option for the incremental 2D case:

``````background: url(/amur_leopard.jpg)
/* background-position */ calc(50% + var(--x) - .5*(var(--i) + 1)*var(--u))  calc(50% + var(--y) - .5*(var(--j) + 1)*var(--u)) /
#{\$d} /* background-size */ padding-box /* background-origin */ content-box /* background-clip */``````

See the Pen by thebabydino (@thebabydino) on CodePen.

There are probably more variations we could be coming up with, but we’ll stop here. If you have more ideas on how to push this further, I’d love to hear about them!

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Top