Solving the Last Item Problem for a Circular Distribution with Partially Overlapping Items
Let’s say we wanted to have something like this:

At first, this doesn’t seem too complicated. We start with 12
numbered items:
- 12.times do |i|
.item #{i}
We give these items dimensions, position them absolutely in the middle of their container, give them a background
, a box-shadow
(or a border
) and tweak the text-related properties a bit so that everything looks nice.
$d: 2em;
.item {
position: absolute;
margin: calc(50vh - #{.5*$d}) 0 0 calc(50vw - #{.5*$d});
width: $d; height: $d;
box-shadow: inset 0 0 0 4px;
background: gainsboro;
font: 900 2em/ #{$d} trebuchet ms, tahoma, verdana, sans-serif;
text-align: center;
}
So far, so good:
See the Pen by thebabydino (@thebabydino) on CodePen.
Now all that’s left is to distribute them on a circle, right? We get a base angle $ba
for our distribution, we rotate each item by its index times this $ba
angle and then translate it along its x
axis:
$n: 12;
$ba: 360deg/$n;
.item {
transform: rotate(var(--a, 0deg)) translate(1.5*$d);
@for $i from 1 to $n { &:nth-child(#{$i + 1}) { --a: $i*$ba } }
}
The result seems fine at first:
See the Pen by thebabydino (@thebabydino) on CodePen.
However, on closer inspection, we notice that we have a problem: item 11
is above both item 0
and item 10
, while item 0
is below both item 1
and 11
:

There are a number of ways to get around this, but they feel kind of hacky and tedious because they involve either duplicating elements, cutting corners with clip-path
, adding pseudo-elements to cover the corners or cut them out via overflow
. Some of these are particularly inefficient if we also need to animate the position of the items or if we want the items to be semi transparent.
So, what’s the best solution then?
3D to the rescue! A really neat thing we can do in this case is to rotate these items in 3D such that their top part goes towards the back (behind the plane of the screen) and their bottom part comes forward (in front of the plane of the screen). We do this by chaining a third transform
function – a rotateX()
:
transform: rotate(var(--a, 0deg)) translate(1.5*$d) rotateX(40deg)
At this point, nothing seems to have changed for the better – we still have the same problem as before and, in addition to that, our items appear to have shrunk along their y
axes, which isn’t something we wanted.
See the Pen by thebabydino (@thebabydino) on CodePen.
Let’s tackle these issues one by one. First off, we need to make all our items belong to the same 3D rendering context and we do this by setting transform-style: preserve-3d
on their parent (which in this case happens to be the body
element).

Those on current Firefox may have noticed we have a different kind of issue now. Item 8
appears both above the previous one (7
) and above the next one (9
), while item 7
appears both below the previous one (6
) and below the next one (8
).

This doesn’t happen in Chrome or in Edge and it’s due to a known Firefox bug where 3D transformed elements are not always rendered in the correct 3D order. Fortunately, this is now fixed in Nightly (55).
Now let’s move on to the issue of the shrinking height. If we look at the first item from the side after the last rotation, this is what we see:
The AB line, rotated at 40°
away from the vertical is the actual height
of our item (h
). The CD line is the projection of this AB line onto the plane of the screen. This is the size we perceive our item’s height to be after the rotation. We want this to be equal to d
, which is also equal to the other dimension of our item (its width
).
We draw a rectangle whose left edge is this projection (CD) and whose top right corner is the A point. Since the opposing edges in a rectangle are equal, the right edge AF of this rectangle equals the projection d
. Since the opposing edges of a rectangle are also parallel, we also get that the ∠OAF (or ∠BAF, same thing) angle equals the ∠AOC angle (they’re alternate angles).
Now let’s remove everything but the right triangle AFB. In this triangle, the AB hypotenuse has a length of h
, the ∠BAF angle is a 40°
one and the AF cathetus is d
.
From here, we have that the cosine of the ∠BAF angle is d/h
:
cos(40°) = d/h → h = d/cos(40°)
So the first thing that comes to mind is that, if we want the projection of our items to look as tall as it is wide, we need to give it a height of $d/cos(40deg)
. However, this doesn’t fix the squished text or any squished backgrounds, so it’s a better idea to leave it with its initial height: $d
and to chain another transform
– a scaleY()
using a factor of 1/cos(40deg)
. Even better, we can store the rotation angle into a variable $ax
and then we have:
$d: 2em;
$ax: 40deg;
.item {
transform: rotate(var(--a, 0deg)) translate(1.5*$d) rotateX($ax) scaleY(1/cos($ax));
}
The above changes give us the desired result (well, in browsers that support CSS variables and don’t have 3D order issues):

This method is really convenient because it doesn’t require us to do anything different for any one item in particular and it works nicely, without any other extra tweaks needed, in the case of semitransparent items. However, the above demo isn’t too exciting, so let’s take a look at a few slightly more interesting use cases.
Note that the following demos only work in WebKit browsers, but this is not something related to the method presented in the article, it’s just a result of the currently poor support of calc()
for anything other than length values.
The first is a tic toc loader, which is a pure CSS recreation of a gif from the Geometric Animations tumblr. The animation is pretty fast in this case, so it may be a bit hard hard to notice the effect here. It only works in WebKit browsers as Firefox and Edge don’t support calc()
as an animation-delay
value and Firefox doesn’t support calc()
in rgb()
either.

The second is a sea shell loader, also a pure CSS recreation of a gif from the same Tumblr and also WebKit only for the same reasons as the previous one.

The third demo is a diagram. It only works in WebKit browsers because Firefox and Edge don’t support calc()
values inside rotate()
functions and Firefox doesn’t support calc()
inside hsl()
either:

The fourth is a circular image gallery, WebKit only for the same reason as the diagram above.

The fifth and last is another loading animation, this time inspired by the Disc Buddies .gif by Dave Whyte.

Solving the Last Item Problem for a Circular Distribution with Partially Overlapping Items is a post from CSS-Tricks
LEAVE A REPLY
You must be logged in to post a comment.