30 CSS Text Animations30 / 30
CSS Rotating Text Wheel Vertical Loop
The rotating value-prop, both flavors: a masked vertical wheel that steps through four words with hold-and-snap pacing (fifth item duplicates the first for a seamless wrap), and a true 3D drum where words turn on a cylinder under perspective.
Published
The code
<div class="cat-30-group">
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&display=swap" rel="stylesheet">
<section class="cat-30a" aria-label="Rotating word wheel headline">
<div class="cat-30a__stage">
<h1 class="cat-30a__line" aria-label="We build websites, apps, brands, futures">We build
<span class="cat-30a__win" aria-hidden="true"><span class="cat-30a__col"><em>websites</em><em>apps</em><em>brands</em><em>futures</em><em>websites</em></span></span>
</h1>
<p class="cat-30a__sub" aria-hidden="true">hold 70% · snap 30% · fifth row duplicates the first</p>
</div>
</section>
<link href="https://fonts.googleapis.com/css2?family=Archivo:ital,wght@0,500..900;1,500..900&display=swap" rel="stylesheet">
<section class="cat-30b" aria-label="3D drum of rotating words">
<div class="cat-30b__stage">
<h1 class="cat-30b__line" aria-label="Design, develop, deploy, delight">
<span class="cat-30b__scene" aria-hidden="true"><span class="cat-30b__drum"><span class="cat-30b__face" style="--n:0">DESIGN</span><span class="cat-30b__face" style="--n:1">DEVELOP</span><span class="cat-30b__face" style="--n:2">DEPLOY</span><span class="cat-30b__face" style="--n:3">DELIGHT</span></span></span>
</h1>
<p class="cat-30b__sub" aria-hidden="true">four faces · rotateX(n·90°) translateZ(.68em) · perspective 900px</p>
</div>
</section>
</div><div class="cat-30-group">
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&display=swap" rel="stylesheet">
<section class="cat-30a" aria-label="Rotating word wheel headline">
<div class="cat-30a__stage">
<h1 class="cat-30a__line" aria-label="We build websites, apps, brands, futures">We build
<span class="cat-30a__win" aria-hidden="true"><span class="cat-30a__col"><em>websites</em><em>apps</em><em>brands</em><em>futures</em><em>websites</em></span></span>
</h1>
<p class="cat-30a__sub" aria-hidden="true">hold 70% · snap 30% · fifth row duplicates the first</p>
</div>
</section>
<link href="https://fonts.googleapis.com/css2?family=Archivo:ital,wght@0,500..900;1,500..900&display=swap" rel="stylesheet">
<section class="cat-30b" aria-label="3D drum of rotating words">
<div class="cat-30b__stage">
<h1 class="cat-30b__line" aria-label="Design, develop, deploy, delight">
<span class="cat-30b__scene" aria-hidden="true"><span class="cat-30b__drum"><span class="cat-30b__face" style="--n:0">DESIGN</span><span class="cat-30b__face" style="--n:1">DEVELOP</span><span class="cat-30b__face" style="--n:2">DEPLOY</span><span class="cat-30b__face" style="--n:3">DELIGHT</span></span></span>
</h1>
<p class="cat-30b__sub" aria-hidden="true">four faces · rotateX(n·90°) translateZ(.68em) · perspective 900px</p>
</div>
</section>
</div>.cat-30-group {
display: flex;
flex-wrap: wrap;
align-items: stretch;
width: 100%;
}
.cat-30-group > section {
flex: 1 1 480px;
min-width: min(100%,360px);
}
.cat-30a,
.cat-30a *,
.cat-30a *::before,
.cat-30a *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cat-30a {
--accent: oklch(0.8 0.16 165);
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: #0e1116;
font-family: 'Space Grotesk','Segoe UI',system-ui,sans-serif;
}
.cat-30a__stage {
text-align: center;
}
.cat-30a__line {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: baseline;
gap: .34em;
font-size: clamp(32px,5.8vw,68px);
font-weight: 600;
color: #edf2fa;
letter-spacing: -.015em;
}
.cat-30a__win {
display: inline-block;
height: 1.2em;
line-height: 1.2;
overflow: hidden;
vertical-align: baseline;
-webkit-mask-image: linear-gradient(180deg,transparent 0,#000 18%,#000 82%,transparent 100%);
mask-image: linear-gradient(180deg,transparent 0,#000 18%,#000 82%,transparent 100%);
}
.cat-30a__col {
display: flex;
flex-direction: column;
animation: cat-30a-wheel 8s cubic-bezier(.6,-0.2,.35,1.2) infinite;
}
.cat-30a__col em {
font-style: normal;
font-weight: 700;
height: 1.2em;
color: var(--accent);
border-bottom: 3px solid color-mix(in oklab,var(--accent) 45%,transparent);
text-align: left;
}
.cat-30a__sub {
margin-top: 22px;
font-family: ui-monospace,Menlo,monospace;
font-size: 11px;
letter-spacing: .16em;
color: rgba(180,200,230,.4);
}
@keyframes cat-30a-wheel {
0%,
17% {
transform: translateY(0);
}
25%,
42% {
transform: translateY(-1.2em);
}
50%,
67% {
transform: translateY(-2.4em);
}
75%,
92% {
transform: translateY(-3.6em);
}
100% {
transform: translateY(-4.8em);
}
}
@media (prefers-reduced-motion: reduce) {
.cat-30a__col {
animation: none;
}
}
.cat-30b,
.cat-30b *,
.cat-30b *::before,
.cat-30b *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cat-30b {
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: linear-gradient(165deg,#151018,#0c0a10);
}
.cat-30b__stage {
text-align: center;
}
.cat-30b__line {
display: grid;
justify-items: center;
font-family: 'Archivo','Segoe UI',system-ui,sans-serif;
}
.cat-30b__scene {
display: inline-block;
perspective: 900px;
height: 1.35em;
font-size: clamp(44px,8.4vw,104px);
line-height: 1.35;
font-weight: 900;
letter-spacing: .02em;
}
.cat-30b__drum {
position: relative;
display: inline-block;
min-width: 6.2ch;
height: 100%;
transform-style: preserve-3d;
animation: cat-30b-roll 9s cubic-bezier(.65,-0.25,.3,1.25) infinite;
}
.cat-30b__face {
position: absolute;
inset: 0;
display: grid;
place-items: center;
backface-visibility: hidden;
transform: rotateX(calc(var(--n)*90deg)) translateZ(.68em);
background: linear-gradient(180deg,oklch(0.85 0.17 330),oklch(0.7 0.2 290));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.cat-30b__sub {
margin-top: 24px;
font-family: ui-monospace,Menlo,monospace;
font-size: 11px;
letter-spacing: .15em;
color: rgba(215,195,235,.4);
}
@keyframes cat-30b-roll {
0%,
17% {
transform: rotateX(0);
}
25%,
42% {
transform: rotateX(-90deg);
}
50%,
67% {
transform: rotateX(-180deg);
}
75%,
92% {
transform: rotateX(-270deg);
}
100% {
transform: rotateX(-360deg);
}
}
@media (prefers-reduced-motion: reduce) {
.cat-30b__drum {
animation: none;
}
}.cat-30-group {
display: flex;
flex-wrap: wrap;
align-items: stretch;
width: 100%;
}
.cat-30-group > section {
flex: 1 1 480px;
min-width: min(100%,360px);
}
.cat-30a,
.cat-30a *,
.cat-30a *::before,
.cat-30a *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cat-30a {
--accent: oklch(0.8 0.16 165);
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: #0e1116;
font-family: 'Space Grotesk','Segoe UI',system-ui,sans-serif;
}
.cat-30a__stage {
text-align: center;
}
.cat-30a__line {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: baseline;
gap: .34em;
font-size: clamp(32px,5.8vw,68px);
font-weight: 600;
color: #edf2fa;
letter-spacing: -.015em;
}
.cat-30a__win {
display: inline-block;
height: 1.2em;
line-height: 1.2;
overflow: hidden;
vertical-align: baseline;
-webkit-mask-image: linear-gradient(180deg,transparent 0,#000 18%,#000 82%,transparent 100%);
mask-image: linear-gradient(180deg,transparent 0,#000 18%,#000 82%,transparent 100%);
}
.cat-30a__col {
display: flex;
flex-direction: column;
animation: cat-30a-wheel 8s cubic-bezier(.6,-0.2,.35,1.2) infinite;
}
.cat-30a__col em {
font-style: normal;
font-weight: 700;
height: 1.2em;
color: var(--accent);
border-bottom: 3px solid color-mix(in oklab,var(--accent) 45%,transparent);
text-align: left;
}
.cat-30a__sub {
margin-top: 22px;
font-family: ui-monospace,Menlo,monospace;
font-size: 11px;
letter-spacing: .16em;
color: rgba(180,200,230,.4);
}
@keyframes cat-30a-wheel {
0%,
17% {
transform: translateY(0);
}
25%,
42% {
transform: translateY(-1.2em);
}
50%,
67% {
transform: translateY(-2.4em);
}
75%,
92% {
transform: translateY(-3.6em);
}
100% {
transform: translateY(-4.8em);
}
}
@media (prefers-reduced-motion: reduce) {
.cat-30a__col {
animation: none;
}
}
.cat-30b,
.cat-30b *,
.cat-30b *::before,
.cat-30b *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cat-30b {
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: linear-gradient(165deg,#151018,#0c0a10);
}
.cat-30b__stage {
text-align: center;
}
.cat-30b__line {
display: grid;
justify-items: center;
font-family: 'Archivo','Segoe UI',system-ui,sans-serif;
}
.cat-30b__scene {
display: inline-block;
perspective: 900px;
height: 1.35em;
font-size: clamp(44px,8.4vw,104px);
line-height: 1.35;
font-weight: 900;
letter-spacing: .02em;
}
.cat-30b__drum {
position: relative;
display: inline-block;
min-width: 6.2ch;
height: 100%;
transform-style: preserve-3d;
animation: cat-30b-roll 9s cubic-bezier(.65,-0.25,.3,1.25) infinite;
}
.cat-30b__face {
position: absolute;
inset: 0;
display: grid;
place-items: center;
backface-visibility: hidden;
transform: rotateX(calc(var(--n)*90deg)) translateZ(.68em);
background: linear-gradient(180deg,oklch(0.85 0.17 330),oklch(0.7 0.2 290));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.cat-30b__sub {
margin-top: 24px;
font-family: ui-monospace,Menlo,monospace;
font-size: 11px;
letter-spacing: .15em;
color: rgba(215,195,235,.4);
}
@keyframes cat-30b-roll {
0%,
17% {
transform: rotateX(0);
}
25%,
42% {
transform: rotateX(-90deg);
}
50%,
67% {
transform: rotateX(-180deg);
}
75%,
92% {
transform: rotateX(-270deg);
}
100% {
transform: rotateX(-360deg);
}
}
@media (prefers-reduced-motion: reduce) {
.cat-30b__drum {
animation: none;
}
}/* No JavaScript — a 1.2em masked window over a five-row column (last row = first row); the keyframe holds each word ~70% of its segment then snaps exactly one line-height. */ /* No JavaScript — a preserve-3d drum with four gradient-clipped faces at rotateX(n·90deg) translateZ(.68em) rolls one face per hold-and-snap segment. */
/* No JavaScript — a 1.2em masked window over a five-row column (last row = first row); the keyframe holds each word ~70% of its segment then snaps exactly one line-height. */
/* No JavaScript — a preserve-3d drum with four gradient-clipped faces at rotateX(n·90deg) translateZ(.68em) rolls one face per hold-and-snap segment. */Here's a working CSS Text Animation from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CSS Rotating Text Wheel Vertical Loop
Source: https://codefronts.com/motion/css-text-animations/css-elastic-bounce-text-animation/
The rotating value-prop, both flavors: a masked vertical wheel that steps through four words with hold-and-snap pacing (fifth item duplicates the first for a seamless wrap), and a true 3D drum where words turn on a cylinder under perspective.
## HTML
```html
<div class="cat-30-group">
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&display=swap" rel="stylesheet">
<section class="cat-30a" aria-label="Rotating word wheel headline">
<div class="cat-30a__stage">
<h1 class="cat-30a__line" aria-label="We build websites, apps, brands, futures">We build
<span class="cat-30a__win" aria-hidden="true"><span class="cat-30a__col"><em>websites</em><em>apps</em><em>brands</em><em>futures</em><em>websites</em></span></span>
</h1>
<p class="cat-30a__sub" aria-hidden="true">hold 70% · snap 30% · fifth row duplicates the first</p>
</div>
</section>
<link href="https://fonts.googleapis.com/css2?family=Archivo:ital,wght@0,500..900;1,500..900&display=swap" rel="stylesheet">
<section class="cat-30b" aria-label="3D drum of rotating words">
<div class="cat-30b__stage">
<h1 class="cat-30b__line" aria-label="Design, develop, deploy, delight">
<span class="cat-30b__scene" aria-hidden="true"><span class="cat-30b__drum"><span class="cat-30b__face" style="--n:0">DESIGN</span><span class="cat-30b__face" style="--n:1">DEVELOP</span><span class="cat-30b__face" style="--n:2">DEPLOY</span><span class="cat-30b__face" style="--n:3">DELIGHT</span></span></span>
</h1>
<p class="cat-30b__sub" aria-hidden="true">four faces · rotateX(n·90°) translateZ(.68em) · perspective 900px</p>
</div>
</section>
</div>
```
## CSS
```css
.cat-30-group {
display: flex;
flex-wrap: wrap;
align-items: stretch;
width: 100%;
}
.cat-30-group > section {
flex: 1 1 480px;
min-width: min(100%,360px);
}
.cat-30a,
.cat-30a *,
.cat-30a *::before,
.cat-30a *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cat-30a {
--accent: oklch(0.8 0.16 165);
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: #0e1116;
font-family: 'Space Grotesk','Segoe UI',system-ui,sans-serif;
}
.cat-30a__stage {
text-align: center;
}
.cat-30a__line {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: baseline;
gap: .34em;
font-size: clamp(32px,5.8vw,68px);
font-weight: 600;
color: #edf2fa;
letter-spacing: -.015em;
}
.cat-30a__win {
display: inline-block;
height: 1.2em;
line-height: 1.2;
overflow: hidden;
vertical-align: baseline;
-webkit-mask-image: linear-gradient(180deg,transparent 0,#000 18%,#000 82%,transparent 100%);
mask-image: linear-gradient(180deg,transparent 0,#000 18%,#000 82%,transparent 100%);
}
.cat-30a__col {
display: flex;
flex-direction: column;
animation: cat-30a-wheel 8s cubic-bezier(.6,-0.2,.35,1.2) infinite;
}
.cat-30a__col em {
font-style: normal;
font-weight: 700;
height: 1.2em;
color: var(--accent);
border-bottom: 3px solid color-mix(in oklab,var(--accent) 45%,transparent);
text-align: left;
}
.cat-30a__sub {
margin-top: 22px;
font-family: ui-monospace,Menlo,monospace;
font-size: 11px;
letter-spacing: .16em;
color: rgba(180,200,230,.4);
}
@keyframes cat-30a-wheel {
0%,
17% {
transform: translateY(0);
}
25%,
42% {
transform: translateY(-1.2em);
}
50%,
67% {
transform: translateY(-2.4em);
}
75%,
92% {
transform: translateY(-3.6em);
}
100% {
transform: translateY(-4.8em);
}
}
@media (prefers-reduced-motion: reduce) {
.cat-30a__col {
animation: none;
}
}
.cat-30b,
.cat-30b *,
.cat-30b *::before,
.cat-30b *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cat-30b {
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: linear-gradient(165deg,#151018,#0c0a10);
}
.cat-30b__stage {
text-align: center;
}
.cat-30b__line {
display: grid;
justify-items: center;
font-family: 'Archivo','Segoe UI',system-ui,sans-serif;
}
.cat-30b__scene {
display: inline-block;
perspective: 900px;
height: 1.35em;
font-size: clamp(44px,8.4vw,104px);
line-height: 1.35;
font-weight: 900;
letter-spacing: .02em;
}
.cat-30b__drum {
position: relative;
display: inline-block;
min-width: 6.2ch;
height: 100%;
transform-style: preserve-3d;
animation: cat-30b-roll 9s cubic-bezier(.65,-0.25,.3,1.25) infinite;
}
.cat-30b__face {
position: absolute;
inset: 0;
display: grid;
place-items: center;
backface-visibility: hidden;
transform: rotateX(calc(var(--n)*90deg)) translateZ(.68em);
background: linear-gradient(180deg,oklch(0.85 0.17 330),oklch(0.7 0.2 290));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.cat-30b__sub {
margin-top: 24px;
font-family: ui-monospace,Menlo,monospace;
font-size: 11px;
letter-spacing: .15em;
color: rgba(215,195,235,.4);
}
@keyframes cat-30b-roll {
0%,
17% {
transform: rotateX(0);
}
25%,
42% {
transform: rotateX(-90deg);
}
50%,
67% {
transform: rotateX(-180deg);
}
75%,
92% {
transform: rotateX(-270deg);
}
100% {
transform: rotateX(-360deg);
}
}
@media (prefers-reduced-motion: reduce) {
.cat-30b__drum {
animation: none;
}
}
```
## JavaScript
```js
/* No JavaScript — a 1.2em masked window over a five-row column (last row = first row); the keyframe holds each word ~70% of its segment then snaps exactly one line-height. */
/* No JavaScript — a preserve-3d drum with four gradient-clipped faces at rotateX(n·90deg) translateZ(.68em) rolls one face per hold-and-snap segment. */
```Here's a working CSS Text Animation from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CSS Rotating Text Wheel Vertical Loop
Source: https://codefronts.com/motion/css-text-animations/css-elastic-bounce-text-animation/
The rotating value-prop, both flavors: a masked vertical wheel that steps through four words with hold-and-snap pacing (fifth item duplicates the first for a seamless wrap), and a true 3D drum where words turn on a cylinder under perspective.
## HTML
```html
<div class="cat-30-group">
<link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@300..700&display=swap" rel="stylesheet">
<section class="cat-30a" aria-label="Rotating word wheel headline">
<div class="cat-30a__stage">
<h1 class="cat-30a__line" aria-label="We build websites, apps, brands, futures">We build
<span class="cat-30a__win" aria-hidden="true"><span class="cat-30a__col"><em>websites</em><em>apps</em><em>brands</em><em>futures</em><em>websites</em></span></span>
</h1>
<p class="cat-30a__sub" aria-hidden="true">hold 70% · snap 30% · fifth row duplicates the first</p>
</div>
</section>
<link href="https://fonts.googleapis.com/css2?family=Archivo:ital,wght@0,500..900;1,500..900&display=swap" rel="stylesheet">
<section class="cat-30b" aria-label="3D drum of rotating words">
<div class="cat-30b__stage">
<h1 class="cat-30b__line" aria-label="Design, develop, deploy, delight">
<span class="cat-30b__scene" aria-hidden="true"><span class="cat-30b__drum"><span class="cat-30b__face" style="--n:0">DESIGN</span><span class="cat-30b__face" style="--n:1">DEVELOP</span><span class="cat-30b__face" style="--n:2">DEPLOY</span><span class="cat-30b__face" style="--n:3">DELIGHT</span></span></span>
</h1>
<p class="cat-30b__sub" aria-hidden="true">four faces · rotateX(n·90°) translateZ(.68em) · perspective 900px</p>
</div>
</section>
</div>
```
## CSS
```css
.cat-30-group {
display: flex;
flex-wrap: wrap;
align-items: stretch;
width: 100%;
}
.cat-30-group > section {
flex: 1 1 480px;
min-width: min(100%,360px);
}
.cat-30a,
.cat-30a *,
.cat-30a *::before,
.cat-30a *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cat-30a {
--accent: oklch(0.8 0.16 165);
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: #0e1116;
font-family: 'Space Grotesk','Segoe UI',system-ui,sans-serif;
}
.cat-30a__stage {
text-align: center;
}
.cat-30a__line {
display: flex;
flex-wrap: wrap;
justify-content: center;
align-items: baseline;
gap: .34em;
font-size: clamp(32px,5.8vw,68px);
font-weight: 600;
color: #edf2fa;
letter-spacing: -.015em;
}
.cat-30a__win {
display: inline-block;
height: 1.2em;
line-height: 1.2;
overflow: hidden;
vertical-align: baseline;
-webkit-mask-image: linear-gradient(180deg,transparent 0,#000 18%,#000 82%,transparent 100%);
mask-image: linear-gradient(180deg,transparent 0,#000 18%,#000 82%,transparent 100%);
}
.cat-30a__col {
display: flex;
flex-direction: column;
animation: cat-30a-wheel 8s cubic-bezier(.6,-0.2,.35,1.2) infinite;
}
.cat-30a__col em {
font-style: normal;
font-weight: 700;
height: 1.2em;
color: var(--accent);
border-bottom: 3px solid color-mix(in oklab,var(--accent) 45%,transparent);
text-align: left;
}
.cat-30a__sub {
margin-top: 22px;
font-family: ui-monospace,Menlo,monospace;
font-size: 11px;
letter-spacing: .16em;
color: rgba(180,200,230,.4);
}
@keyframes cat-30a-wheel {
0%,
17% {
transform: translateY(0);
}
25%,
42% {
transform: translateY(-1.2em);
}
50%,
67% {
transform: translateY(-2.4em);
}
75%,
92% {
transform: translateY(-3.6em);
}
100% {
transform: translateY(-4.8em);
}
}
@media (prefers-reduced-motion: reduce) {
.cat-30a__col {
animation: none;
}
}
.cat-30b,
.cat-30b *,
.cat-30b *::before,
.cat-30b *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cat-30b {
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: linear-gradient(165deg,#151018,#0c0a10);
}
.cat-30b__stage {
text-align: center;
}
.cat-30b__line {
display: grid;
justify-items: center;
font-family: 'Archivo','Segoe UI',system-ui,sans-serif;
}
.cat-30b__scene {
display: inline-block;
perspective: 900px;
height: 1.35em;
font-size: clamp(44px,8.4vw,104px);
line-height: 1.35;
font-weight: 900;
letter-spacing: .02em;
}
.cat-30b__drum {
position: relative;
display: inline-block;
min-width: 6.2ch;
height: 100%;
transform-style: preserve-3d;
animation: cat-30b-roll 9s cubic-bezier(.65,-0.25,.3,1.25) infinite;
}
.cat-30b__face {
position: absolute;
inset: 0;
display: grid;
place-items: center;
backface-visibility: hidden;
transform: rotateX(calc(var(--n)*90deg)) translateZ(.68em);
background: linear-gradient(180deg,oklch(0.85 0.17 330),oklch(0.7 0.2 290));
-webkit-background-clip: text;
background-clip: text;
color: transparent;
}
.cat-30b__sub {
margin-top: 24px;
font-family: ui-monospace,Menlo,monospace;
font-size: 11px;
letter-spacing: .15em;
color: rgba(215,195,235,.4);
}
@keyframes cat-30b-roll {
0%,
17% {
transform: rotateX(0);
}
25%,
42% {
transform: rotateX(-90deg);
}
50%,
67% {
transform: rotateX(-180deg);
}
75%,
92% {
transform: rotateX(-270deg);
}
100% {
transform: rotateX(-360deg);
}
}
@media (prefers-reduced-motion: reduce) {
.cat-30b__drum {
animation: none;
}
}
```
## JavaScript
```js
/* No JavaScript — a 1.2em masked window over a five-row column (last row = first row); the keyframe holds each word ~70% of its segment then snaps exactly one line-height. */
/* No JavaScript — a preserve-3d drum with four gradient-clipped faces at rotateX(n·90deg) translateZ(.68em) rolls one face per hold-and-snap segment. */
```How this works
The wheel is a 1.2em-tall overflow:hidden window over a stacked word column. One keyframe translates the column by exact line increments, holding ~70% of each segment so words are readable between snaps:
@keyframes wheel{0%,17%{transform:translateY(0)}
25%,42%{transform:translateY(-1.2em)}
50%,67%{transform:translateY(-2.4em)}
75%,92%{transform:translateY(-3.6em)}
100%{transform:translateY(-4.8em)}}Item five is a duplicate of item one, so 100% lands on a frame identical to 0% — the infinite loop never visibly rewinds. The drum variant mounts four faces at rotateX(n·90deg) translateZ(0.68em) inside a preserve-3d element and rotates it −360° with the same hold pacing; backface-visibility:hidden keeps departed words from ghosting.
Make it yours
- Word count: N words → N+1 rows, segments at 100/N%, travel in 1.2em steps.
- Pacing: widen the hold spans (e.g. 0–20%) for slower reading, tighten for energy.
- Style the rotating word only — gradient, underline, chip background — to anchor the eye.
- Drum radius: translateZ = (line-height/2) ÷ tan(45°) for 4 faces; more faces, smaller angles.
Gotchas — read before shipping
- Fix the wheel's width to the longest word (or use inline-grid) so the sentence doesn't reflow every swap.
- em travel must exactly match the row line-height or errors accumulate per cycle.
- Screen readers: put the full proposition in aria-label and hide the mechanical copies.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.2+ | 113+ | 111+ |
Floor set by oklch(), color-mix() as this demo is written. The core technique itself goes back further — Chrome 36+.
Transforms and keyframe holds are universal; the drum needs 3D transform support (equally universal). Reduced motion pins the first word.