Rotating Text
A word in an indigo → magenta gradient, with no box around it, gently sways in 3D (±15° around the vertical axis, ±10° around the horizontal one, 4 s loop) and spins a full turn in 1 s when you hover it or it gets keyboard focus. Pure CSS, zero JavaScript, front-facing and still when the user reduces motion.
Updated
This effect is part of Effect.Labs — 840 vanilla effects, some free, some premium. 3D has 50 effects, including 8 free. Explore the category →
3 usage examples



How it works
The .fx-0008-root root (#0a0a0f background, at least 300 px tall) centers .text-rotate-container, which carries the camera (perspective: 400px, short, so that 15° shows on a single word) and tabindex="0". Inside it, .text-rotate (28 px, weight 800, transform-style: preserve-3d) plays textRotate3D: 4 s, ease-in-out, forever; at 0, 50 and 100% the word faces front, at 25% it turns 15° to the right and tips 10° backward, at 75% the opposite. The result is a slow figure-eight sway, like a hanging sign. The text itself is an inline-block <span> whose background is a 135° #6366f1 → #d946ef gradient, clipped to the letter shapes (background-clip: text, plus its -webkit- version) and revealed by -webkit-text-fill-color: transparent.
The full turn is a second animation, textSpin (rotateY(0) → rotateY(360deg), 1 s, once), which replaces the sway when .text-rotate-container is hovered or has keyboard focus (:focus-visible, 2 px #a5b4fc outline). Once the turn ends, the word stays front-facing while the pointer is on it; it sways again when the pointer leaves. Hover is read on the container, which does not turn, not on the word: seen edge-on mid-turn, the word has almost no surface left, and a hover read on it would get lost. Measured in Chromium 151 and Firefox 153, with the pointer near the edge of the word moving by 3 px: on the container, the turn goes all the way; a hover read on the word itself restarts the turn from zero about every 0.35 s.
The sway only moves one word: 0.44% of the block's pixels change over 0.8 s, so the effect stays subtle. Only transform is animated. Two copies on one page sway and spin independently. The text-clipped gradient does not depend on 3D; the rotation does: Playwright's WebKit flattens all CSS 3D, so rendering on a real Safari was not verified.
Accessibility
- Reduced motion: under
prefers-reduced-motion: reduce, a block scoped to.fx-0008-rootremoves the sway and the turn: the word stays front-facing. On hover and focus, it brightens (filter: brightness(1.35)) instead of turning. Measured in Chromium and Firefox: 0 running animations, 0% of pixels changing over 0.8 s, no animation on hover. - Readability: it is real text, read as-is by screen readers and selectable. The gradient runs from 4.4:1 (indigo) to 5.7:1 (magenta) on the
#0a0a0fbackground, above the 3:1 threshold for 28 px bold text. During the turn the word is unreadable for half a second: do not use it for information people need to read at hover time. - Keyboard: the container has
tabindex="0", and the turn starts on keyboard focus with a visible outline. If the word is a link, remove thattabindexand target the link instead (a:focus-visible .text-rotate): a focusable element that does nothing adds a useless stop to keyboard navigation. - Touch: on a phone, tapping the word triggers the browser's emulated hover, hence one turn; the sway runs with no gesture at all.
Browser compatibility
CSS (3D transform, perspective, background-clip: text, -webkit-text-fill-color, @keyframes), zero JavaScript, no dependencies. Measured in Chromium 151 and Firefox 153: same sway (0.68% difference on the still frame, due to text antialiasing), full turn on hover and focus, two independent copies, 0 errors. Playwright's WebKit 26.5 flattens all CSS 3D: the real Safari was not verified.
Without CSS 3D, the word stays flat and the sway shrinks to a slight horizontal stretch. Without background-clip: text, -webkit-text-fill-color is usually ignored as well and the word shows in the text color (white), without the gradient.
The code
HTML structure to paste into your page (the CSS + JS come with your purchase):
<div class="fx-0008-root">
<div class="demo-3d">
<div class="text-rotate-container" tabindex="0">
<div class="text-rotate">
<span>3D TEXT</span>
</div>
</div>
</div>
</div>
One-time payment, no subscription — the code is yours forever
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
Sway amplitude (CSS — @keyframes textRotate3D, 25% and 75% steps) |
rotateY ±15°, rotateX ±10° | ±8° / ±5° for a word that barely breathes; ±25° / ±15° for a sign battered by the wind. Keep 0% = 50% = 100% front-facing: the loop goes through rest twice. |
Sway duration (CSS — 4s in the animation of .text-rotate) |
4 s, ease-in-out | 6 s for a heavy sign, 2.5 s for a livelier effect. ease-in-out slows down at the ends, like a hanging object. |
Perspective (CSS — perspective on .text-rotate-container) |
400px | Kept short, it makes small angles visible. 800 px flattens the sway; 250 px makes it very pronounced, at the risk of distorting long words. |
Size and font (CSS — font-size and font-weight of .text-rotate) |
28 px, 800 | A 40 to 50 px word shows the 3D better. Bold letters keep enough surface for the gradient to read. |
Gradient (CSS — background of .text-rotate span) |
linear-gradient(135deg, #6366f1, #d946ef) | Change only the image (background-image) to keep the text clipping. Check the contrast of the darkest color against your background. |
Turn on hover (CSS — 1s and textSpin in the :hover, :focus-visible rule) |
one turn in 1 s, once | 1.6s for a more solemn turn. For two turns, write rotateY(720deg) in textSpin. Delete the rule to keep only the sway. |
FAQ
<a href="…"> in place of the <span> or around the container, remove tabindex="0" and add a:focus-visible .text-rotate to the turn rule. Keep the target large enough: 28 px tall is a minimum for a finger.-webkit-background-clip: text then background-clip: text, which covers browsers that only know one of the two forms. In the rare case where neither works, -webkit-text-fill-color is usually ignored too and the word shows in white, readable, without the gradient.