Circular menu design exists as a space-saver or choice, and there’s an easy and efficient way to create and animate it in CSS using offset
and animation-composition
. Here are some examples (click the button in the center of the choices):
I’ll take you through the second example to cover the basics.
The Layout
Just some semantic HTML here. Since we’re offering a menu of options, a <menu>
seems appropriate (yes, <li>
is correct as a child!) and each button is focusable.
<main>
<div class="menu-wrapper">
<menu>
<li><button>Poland</button></li>
<li><button>Brazil</button></li>
<li><button>Qatar</button></li>
<!-- etc. -->
</menu>
<button class="menu-button" onclick="revolve()">See More</button>
</div>
</main>
Code language: HTML, XML (xml)
Other important bits:
The menu and the menu button (<button id="menu-button">
) are the same size and shape and stacked on top of each other.
Half of the menu is hidden via overflow: clip;
and the menu wrapper being pulled upwards.
main {
overflow: clip;
}
.menu-wrapper {
display: grid;
place-items: center;
transform: translateY(-129px);
menu, .menu-button {
width: 259px;
height: 129px;
grid-area: 1 / 1;
border-radius: 50%;
}
}
Code language: CSS (css)
Set the menu items (<li>
s) around the <menu>
’s center using offset
.
menu {
padding: 30px;
--gap: 10%; /* The in-between gap for the 10 items */
}
li {
offset: padding-box 0deg;
offset-distance: calc((sibling-index() - 1) * var(--gap));
/* or
&:nth-of-type(2) { offset-distance: calc(1 * var(--gap)); }
&:nth-of-type(3) { offset-distance: calc(2 * var(--gap)); }
etc...
*/
}
Code language: CSS (css)
The offset
(a longhand property) positions all the <li>
elements along the <menu>
’s padding-box
that has been set as the offset path.
The offset CSS shorthand property sets all the properties required for animating an element along a defined path. The offset properties together help to define an offset transform, a transform that aligns a point in an element (offset-anchor) to an offset position (offset-position) on a path (offset-path) at various points along the path (offset-distance) and optionally rotates the element (offset-rotate) to follow the direction of the path. — MDN Web Docs
The offset-distance
is set to spread the menu items along the path based on the given gap between them (--gap: 10%
).
Items | Initial value of offset-distance |
---|---|
1 | 0% |
2 | 10% |
3 | 20% |
The Animation
@keyframes rev1 {
to {
offset-distance: 50%;
}
}
@keyframes rev2 {
from {
offset-distance: 50%;
}
to {
offset-distance: 0%;
}
}
Code language: CSS (css)
Set two @keyframes
animations to move the menu items halfway to the left, clockwise, (rev1
), and then from that position back to the right (rev2
)
li {
/* ... */
animation: 1s forwards;
animation-composition: add;
}
Code language: CSS (css)
Set animation-time
(1s
) and animation-direction
(forwards
), and animation-composition
(add
) for the <li>
elements
Even though animations can be triggered in CSS — for example, within a :checked
state — since we’re using a <button>
, the names of the animations will be set in the <button>
’s click handler to trigger the animations.
By using animation-composition
, the animations are made to add, not replace by default, the offset-distance
values inside the @keyframes
rulesets to the initial offset-distance
values of each of the <li>
.
Items | Initial Value | to |
---|---|---|
1 | 0% | (0% + 50%) 50% |
2 | 10% | (10% + 50%) 60% |
3 | 20% | (20% + 50%) 70% |
animation-composition: add
Items | from | back to Initial Value |
---|---|---|
1 | (0% + 50%) 50% | (0% + 0%) 0% |
2 | (10% + 50%) 60% | (10% + 0%) 10% |
3 | (20% + 50%) 70% | (20% + 0%) 20% |
animation-composition: add
Here’s how it would’ve been without animation-composition: add
:
Items | Initial Value | to |
---|---|---|
1 | 0% | 50% |
2 | 10% | 50% |
3 | 20% | 50% |
The animation-composition CSS property specifies the composite operation to use when multiple animations affect the same property simultaneously.
MDN Web Docs
The Trigger
const LI = document.querySelectorAll('li');
let flag = true;
function revolve() {
LI.forEach(li => li.style.animationName = flag ? "rev1" : "rev2");
flag = !flag;
}
Code language: JavaScript (javascript)
In the menu button’s click handler, revolve()
, set the <li>
elements’ animationName
to rev1
and rev2
, alternatively.
Assigning the animation name triggers the corresponding keyframes animation each time the <button>
is clicked.
Using the method covered in this post, it’s possible to control how much along a revolution the elements are to move (demo one), and which direction. You can also experiment with different offset path shapes. You can declare (@keyframes
) and trigger (:checked
, :hover
, etc.) the animations in CSS, or using JavaScript’s Web Animations API that includes the animation composition property.