6 Pure CSS1 with JSPublished 2026-10-05Free · no sign-up
Seven rotations: two gears that mesh, planets on orbits of different speeds, a coin spinning in 3D, a badge with text running round a circle, a refresh icon that spins while it works, a six-faced cube and a disclosure chevron that turns with its open state.
Two gears turn in opposite directions at speeds matched to their sizes, so their teeth appear to mesh. The teeth are a repeating conic gradient masked into a ring, and the smaller gear spins faster in proportion to its tooth count.
Two planets circle a sun on rings of different sizes and speeds. Each orbit is a rotating ring element with the planet placed on its edge, so a single rotate animation moves the planet round the whole circle.
A coin spins on its vertical axis with two faces, heads and tails, using rotateY, preserve-3d and backface-visibility. A thin edge is faked with a box-shadow so it does not vanish completely when side-on.
Text set on a circle with SVG textPath turns slowly around a centre icon, a stamp-like badge for heroes and portfolio sites. The text is real SVG text, and the badge has a single accessible name so it is not read character by character.
View code
<a class="ro4" href="#contact" aria-label="Available for projects, get in touch"><svg viewBox="0 0 100 100" aria-hidden="true"><defs><path id="ro4p" d="M50 50 m-38 0 a38 38 0 1 1 76 0 a38 38 0 1 1 -76 0"/></defs><text><textPath href="#ro4p">AVAILABLE FOR PROJECTS • GET IN TOUCH •</textPath></text></svg><span aria-hidden="true">↗</span></a>
A refresh button whose arrows spin while the refresh runs and stop when it ends. The rotation runs through the button's aria-busy state, and the label changes to Refreshing so the state is in the text as well.
var b = document.querySelector('.ro5'), s = b.querySelector('span');
b.onclick = function () { if (b.getAttribute('aria-busy') === 'true') return; b.setAttribute('aria-busy', 'true'); s.textContent = 'Refreshing…'; setTimeout(function () { b.removeAttribute('aria-busy'); s.textContent = 'Refresh'; }, 1800); };
06. Rotating 3D Cube
Rotate3D
A six-faced cube turning on two axes, built from six squares placed with rotate and translateZ inside a preserve-3d parent. Each face is half the cube's size away from the centre, which is the one number that must match the face size.
Disclosure chevrons that rotate 180 degrees when their details element opens, driven by the [open] attribute, so no script is involved. The rotation is a short transition rather than an animation, and it reverses when the panel closes.
View code
<div class="ro7"><details><summary>Shipping<span aria-hidden="true"></span></summary><p>Orders ship within two working days.</p></details><details><summary>Returns<span aria-hidden="true"></span></summary><p>Free returns within 30 days.</p></details></div>
Rotation happens around transform-origin, the centre by default. Set it to the hinge, the axle or the bottom edge depending on what is turning.
Orbits are rotating parents
To move something round a circle, put it on the edge of a ring element and rotate the ring. One animation does the whole orbit, no maths needed.
3D needs three things
perspective on the parent, transform-style: preserve-3d on the rotating element, and backface-visibility: hidden on faces that should disappear when turned away.
Linear for loops, eased for one-offs
Continuous spins use linear timing so the speed never wobbles. Single turns, like a chevron or a gear on hover, feel better with ease-out.
Frequently asked questions
Can I rotate text on a circle without SVG?
Not easily. SVG textPath is the reliable way, as in the badge demo.
Why is the coin edge visible side-on?
A 3D coin has no thickness in CSS; an inset shadow fakes a rim so it does not vanish completely.
Do spinning elements need reduced motion handling?
Yes. Each demo stops or slows its rotation under prefers-reduced-motion.