26 CSS Link Effects25 / 26
CSS 3D Text Flip Hover Effect
Text with a physical edge: hover rolls the label 90° like the face of a cube, revealing an accent copy that was hiding on top. One word rolling whole, a per-letter cascade that ripples across the label 50ms at a time, and a hinge variant that swings down a translation ('Hello' → 'Bonjour'). Real perspective, two text nodes, zero JS.
Published Updated
The code
<section class="cle-25" aria-label="3D text flip hover demo">
<div class="cle-25__stage">
<div class="cle-25__var"><a href="#a" class="cle-25__flip"><span class="cle-25__box" data-text="Portfolio"><span class="cle-25__f">Portfolio</span></span></a><small class="cle-25__cap">A · whole-word roll</small></div>
<div class="cle-25__var">
<a href="#b" class="cle-25__cascade" aria-label="Showreel"><span class="cle-25__w" aria-hidden="true"><span class="cle-25__box" data-text="S" style="--i:0"><span class="cle-25__f">S</span></span><span class="cle-25__box" data-text="H" style="--i:1"><span class="cle-25__f">H</span></span><span class="cle-25__box" data-text="O" style="--i:2"><span class="cle-25__f">O</span></span><span class="cle-25__box" data-text="W" style="--i:3"><span class="cle-25__f">W</span></span><span class="cle-25__box" data-text="R" style="--i:4"><span class="cle-25__f">R</span></span><span class="cle-25__box" data-text="E" style="--i:5"><span class="cle-25__f">E</span></span><span class="cle-25__box" data-text="E" style="--i:6"><span class="cle-25__f">E</span></span><span class="cle-25__box" data-text="L" style="--i:7"><span class="cle-25__f">L</span></span></span></a>
<small class="cle-25__cap">B · per-letter cascade</small>
</div>
<div class="cle-25__var"><a href="#c" class="cle-25__hinge"><span class="cle-25__h1">Hello</span><span class="cle-25__h2" aria-hidden="true">Bonjour</span></a><small class="cle-25__cap">C · hinge swap</small></div>
</div>
</section><section class="cle-25" aria-label="3D text flip hover demo">
<div class="cle-25__stage">
<div class="cle-25__var"><a href="#a" class="cle-25__flip"><span class="cle-25__box" data-text="Portfolio"><span class="cle-25__f">Portfolio</span></span></a><small class="cle-25__cap">A · whole-word roll</small></div>
<div class="cle-25__var">
<a href="#b" class="cle-25__cascade" aria-label="Showreel"><span class="cle-25__w" aria-hidden="true"><span class="cle-25__box" data-text="S" style="--i:0"><span class="cle-25__f">S</span></span><span class="cle-25__box" data-text="H" style="--i:1"><span class="cle-25__f">H</span></span><span class="cle-25__box" data-text="O" style="--i:2"><span class="cle-25__f">O</span></span><span class="cle-25__box" data-text="W" style="--i:3"><span class="cle-25__f">W</span></span><span class="cle-25__box" data-text="R" style="--i:4"><span class="cle-25__f">R</span></span><span class="cle-25__box" data-text="E" style="--i:5"><span class="cle-25__f">E</span></span><span class="cle-25__box" data-text="E" style="--i:6"><span class="cle-25__f">E</span></span><span class="cle-25__box" data-text="L" style="--i:7"><span class="cle-25__f">L</span></span></span></a>
<small class="cle-25__cap">B · per-letter cascade</small>
</div>
<div class="cle-25__var"><a href="#c" class="cle-25__hinge"><span class="cle-25__h1">Hello</span><span class="cle-25__h2" aria-hidden="true">Bonjour</span></a><small class="cle-25__cap">C · hinge swap</small></div>
</div>
</section>.cle-25,
.cle-25 *,
.cle-25 *::before,
.cle-25 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cle-25 {
width: 100%;
min-height: 100vh;
--ink: oklch(0.93 0.01 80);
--accent: oklch(0.8 0.16 80);
font-family: 'Segoe UI',system-ui,sans-serif;
}
.cle-25__stage {
width: 100%;
height: 100vh;
display: grid;
align-content: center;
justify-items: center;
gap: 52px;
background: linear-gradient(170deg,oklch(0.26 0.02 60),oklch(0.18 0.015 50));
padding: 44px 32px;
}
.cle-25__var {
display: grid;
gap: 15px;
justify-items: center;
}
.cle-25__cap {
font-size: 10.5px;
font-weight: 600;
letter-spacing: .14em;
text-transform: uppercase;
color: oklch(0.6 0.04 70);
}
.cle-25 a {
display: inline-block;
perspective: 600px;
color: var(--ink);
text-decoration: none;
font-size: clamp(28px,4.4vw,40px);
font-weight: 800;
letter-spacing: -.01em;
line-height: 1.1;
}
.cle-25 a:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 8px;
border-radius: 4px;
}
.cle-25__box {
position: relative;
display: inline-block;
transform-style: preserve-3d;
transition: transform .5s cubic-bezier(.35,1.2,.35,1);
}
.cle-25__f {
display: inline-block;
transform: translateZ(.55em);
opacity: 1;
transition: opacity .2s .12s;
}
.cle-25__box::before {
content: attr(data-text);
position: absolute;
inset: 0;
color: var(--accent);
transform: rotateX(90deg) translateZ(.55em);
opacity: 0;
transition: opacity .2s .12s;
}
.cle-25__flip:hover .cle-25__box::before,
.cle-25__flip:focus-visible .cle-25__box::before,
.cle-25__cascade:hover .cle-25__box::before,
.cle-25__cascade:focus-visible .cle-25__box::before {
opacity: 1;
}
.cle-25__flip:hover .cle-25__f,
.cle-25__flip:focus-visible .cle-25__f,
.cle-25__cascade:hover .cle-25__f,
.cle-25__cascade:focus-visible .cle-25__f {
opacity: 0;
}
.cle-25__cascade .cle-25__box::before,
.cle-25__cascade .cle-25__f {
transition-delay: calc(var(--i) * 50ms + 120ms);
}
.cle-25__flip:hover .cle-25__box,
.cle-25__flip:focus-visible .cle-25__box {
transform: rotateX(-90deg);
}
.cle-25__cascade .cle-25__w {
display: inline-block;
letter-spacing: .05em;
}
.cle-25__cascade .cle-25__box {
transition-delay: calc(var(--i) * 50ms);
}
.cle-25__cascade:hover .cle-25__box,
.cle-25__cascade:focus-visible .cle-25__box {
transform: rotateX(-90deg);
}
.cle-25__hinge {
display: inline-grid;
}
.cle-25__hinge > span {
grid-area: 1/1;
display: inline-block;
backface-visibility: hidden;
transition: transform .45s cubic-bezier(.35,1.1,.35,1),opacity .3s;
}
.cle-25__h1 {
transform-origin: 50% 0;
transform: rotateX(0);
}
.cle-25__h2 {
color: var(--accent);
transform-origin: 50% 100%;
transform: rotateX(-90deg);
opacity: 0;
}
.cle-25__hinge:hover .cle-25__h1,
.cle-25__hinge:focus-visible .cle-25__h1 {
transform: rotateX(90deg);
opacity: 0;
}
.cle-25__hinge:hover .cle-25__h2,
.cle-25__hinge:focus-visible .cle-25__h2 {
transform: rotateX(0);
opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
.cle-25 * {
transition-duration: .01s !important;
transition-delay: 0s !important;
}
}.cle-25,
.cle-25 *,
.cle-25 *::before,
.cle-25 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cle-25 {
width: 100%;
min-height: 100vh;
--ink: oklch(0.93 0.01 80);
--accent: oklch(0.8 0.16 80);
font-family: 'Segoe UI',system-ui,sans-serif;
}
.cle-25__stage {
width: 100%;
height: 100vh;
display: grid;
align-content: center;
justify-items: center;
gap: 52px;
background: linear-gradient(170deg,oklch(0.26 0.02 60),oklch(0.18 0.015 50));
padding: 44px 32px;
}
.cle-25__var {
display: grid;
gap: 15px;
justify-items: center;
}
.cle-25__cap {
font-size: 10.5px;
font-weight: 600;
letter-spacing: .14em;
text-transform: uppercase;
color: oklch(0.6 0.04 70);
}
.cle-25 a {
display: inline-block;
perspective: 600px;
color: var(--ink);
text-decoration: none;
font-size: clamp(28px,4.4vw,40px);
font-weight: 800;
letter-spacing: -.01em;
line-height: 1.1;
}
.cle-25 a:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 8px;
border-radius: 4px;
}
.cle-25__box {
position: relative;
display: inline-block;
transform-style: preserve-3d;
transition: transform .5s cubic-bezier(.35,1.2,.35,1);
}
.cle-25__f {
display: inline-block;
transform: translateZ(.55em);
opacity: 1;
transition: opacity .2s .12s;
}
.cle-25__box::before {
content: attr(data-text);
position: absolute;
inset: 0;
color: var(--accent);
transform: rotateX(90deg) translateZ(.55em);
opacity: 0;
transition: opacity .2s .12s;
}
.cle-25__flip:hover .cle-25__box::before,
.cle-25__flip:focus-visible .cle-25__box::before,
.cle-25__cascade:hover .cle-25__box::before,
.cle-25__cascade:focus-visible .cle-25__box::before {
opacity: 1;
}
.cle-25__flip:hover .cle-25__f,
.cle-25__flip:focus-visible .cle-25__f,
.cle-25__cascade:hover .cle-25__f,
.cle-25__cascade:focus-visible .cle-25__f {
opacity: 0;
}
.cle-25__cascade .cle-25__box::before,
.cle-25__cascade .cle-25__f {
transition-delay: calc(var(--i) * 50ms + 120ms);
}
.cle-25__flip:hover .cle-25__box,
.cle-25__flip:focus-visible .cle-25__box {
transform: rotateX(-90deg);
}
.cle-25__cascade .cle-25__w {
display: inline-block;
letter-spacing: .05em;
}
.cle-25__cascade .cle-25__box {
transition-delay: calc(var(--i) * 50ms);
}
.cle-25__cascade:hover .cle-25__box,
.cle-25__cascade:focus-visible .cle-25__box {
transform: rotateX(-90deg);
}
.cle-25__hinge {
display: inline-grid;
}
.cle-25__hinge > span {
grid-area: 1/1;
display: inline-block;
backface-visibility: hidden;
transition: transform .45s cubic-bezier(.35,1.1,.35,1),opacity .3s;
}
.cle-25__h1 {
transform-origin: 50% 0;
transform: rotateX(0);
}
.cle-25__h2 {
color: var(--accent);
transform-origin: 50% 100%;
transform: rotateX(-90deg);
opacity: 0;
}
.cle-25__hinge:hover .cle-25__h1,
.cle-25__hinge:focus-visible .cle-25__h1 {
transform: rotateX(90deg);
opacity: 0;
}
.cle-25__hinge:hover .cle-25__h2,
.cle-25__hinge:focus-visible .cle-25__h2 {
transform: rotateX(0);
opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
.cle-25 * {
transition-duration: .01s !important;
transition-delay: 0s !important;
}
}Here's a working CSS Link Effect 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 3D Text Flip Hover Effect
Source: https://codefronts.com/motion/css-link-effects/3d-flip-reveal/
Text with a physical edge: hover rolls the label 90° like the face of a cube, revealing an accent copy that was hiding on top. One word rolling whole, a per-letter cascade that ripples across the label 50ms at a time, and a hinge variant that swings down a translation ('Hello' → 'Bonjour'). Real perspective, two text nodes, zero JS.
## HTML
```html
<section class="cle-25" aria-label="3D text flip hover demo">
<div class="cle-25__stage">
<div class="cle-25__var"><a href="#a" class="cle-25__flip"><span class="cle-25__box" data-text="Portfolio"><span class="cle-25__f">Portfolio</span></span></a><small class="cle-25__cap">A · whole-word roll</small></div>
<div class="cle-25__var">
<a href="#b" class="cle-25__cascade" aria-label="Showreel"><span class="cle-25__w" aria-hidden="true"><span class="cle-25__box" data-text="S" style="--i:0"><span class="cle-25__f">S</span></span><span class="cle-25__box" data-text="H" style="--i:1"><span class="cle-25__f">H</span></span><span class="cle-25__box" data-text="O" style="--i:2"><span class="cle-25__f">O</span></span><span class="cle-25__box" data-text="W" style="--i:3"><span class="cle-25__f">W</span></span><span class="cle-25__box" data-text="R" style="--i:4"><span class="cle-25__f">R</span></span><span class="cle-25__box" data-text="E" style="--i:5"><span class="cle-25__f">E</span></span><span class="cle-25__box" data-text="E" style="--i:6"><span class="cle-25__f">E</span></span><span class="cle-25__box" data-text="L" style="--i:7"><span class="cle-25__f">L</span></span></span></a>
<small class="cle-25__cap">B · per-letter cascade</small>
</div>
<div class="cle-25__var"><a href="#c" class="cle-25__hinge"><span class="cle-25__h1">Hello</span><span class="cle-25__h2" aria-hidden="true">Bonjour</span></a><small class="cle-25__cap">C · hinge swap</small></div>
</div>
</section>
```
## CSS
```css
.cle-25,
.cle-25 *,
.cle-25 *::before,
.cle-25 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cle-25 {
width: 100%;
min-height: 100vh;
--ink: oklch(0.93 0.01 80);
--accent: oklch(0.8 0.16 80);
font-family: 'Segoe UI',system-ui,sans-serif;
}
.cle-25__stage {
width: 100%;
height: 100vh;
display: grid;
align-content: center;
justify-items: center;
gap: 52px;
background: linear-gradient(170deg,oklch(0.26 0.02 60),oklch(0.18 0.015 50));
padding: 44px 32px;
}
.cle-25__var {
display: grid;
gap: 15px;
justify-items: center;
}
.cle-25__cap {
font-size: 10.5px;
font-weight: 600;
letter-spacing: .14em;
text-transform: uppercase;
color: oklch(0.6 0.04 70);
}
.cle-25 a {
display: inline-block;
perspective: 600px;
color: var(--ink);
text-decoration: none;
font-size: clamp(28px,4.4vw,40px);
font-weight: 800;
letter-spacing: -.01em;
line-height: 1.1;
}
.cle-25 a:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 8px;
border-radius: 4px;
}
.cle-25__box {
position: relative;
display: inline-block;
transform-style: preserve-3d;
transition: transform .5s cubic-bezier(.35,1.2,.35,1);
}
.cle-25__f {
display: inline-block;
transform: translateZ(.55em);
opacity: 1;
transition: opacity .2s .12s;
}
.cle-25__box::before {
content: attr(data-text);
position: absolute;
inset: 0;
color: var(--accent);
transform: rotateX(90deg) translateZ(.55em);
opacity: 0;
transition: opacity .2s .12s;
}
.cle-25__flip:hover .cle-25__box::before,
.cle-25__flip:focus-visible .cle-25__box::before,
.cle-25__cascade:hover .cle-25__box::before,
.cle-25__cascade:focus-visible .cle-25__box::before {
opacity: 1;
}
.cle-25__flip:hover .cle-25__f,
.cle-25__flip:focus-visible .cle-25__f,
.cle-25__cascade:hover .cle-25__f,
.cle-25__cascade:focus-visible .cle-25__f {
opacity: 0;
}
.cle-25__cascade .cle-25__box::before,
.cle-25__cascade .cle-25__f {
transition-delay: calc(var(--i) * 50ms + 120ms);
}
.cle-25__flip:hover .cle-25__box,
.cle-25__flip:focus-visible .cle-25__box {
transform: rotateX(-90deg);
}
.cle-25__cascade .cle-25__w {
display: inline-block;
letter-spacing: .05em;
}
.cle-25__cascade .cle-25__box {
transition-delay: calc(var(--i) * 50ms);
}
.cle-25__cascade:hover .cle-25__box,
.cle-25__cascade:focus-visible .cle-25__box {
transform: rotateX(-90deg);
}
.cle-25__hinge {
display: inline-grid;
}
.cle-25__hinge > span {
grid-area: 1/1;
display: inline-block;
backface-visibility: hidden;
transition: transform .45s cubic-bezier(.35,1.1,.35,1),opacity .3s;
}
.cle-25__h1 {
transform-origin: 50% 0;
transform: rotateX(0);
}
.cle-25__h2 {
color: var(--accent);
transform-origin: 50% 100%;
transform: rotateX(-90deg);
opacity: 0;
}
.cle-25__hinge:hover .cle-25__h1,
.cle-25__hinge:focus-visible .cle-25__h1 {
transform: rotateX(90deg);
opacity: 0;
}
.cle-25__hinge:hover .cle-25__h2,
.cle-25__hinge:focus-visible .cle-25__h2 {
transform: rotateX(0);
opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
.cle-25 * {
transition-duration: .01s !important;
transition-delay: 0s !important;
}
}
```Here's a working CSS Link Effect 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 3D Text Flip Hover Effect
Source: https://codefronts.com/motion/css-link-effects/3d-flip-reveal/
Text with a physical edge: hover rolls the label 90° like the face of a cube, revealing an accent copy that was hiding on top. One word rolling whole, a per-letter cascade that ripples across the label 50ms at a time, and a hinge variant that swings down a translation ('Hello' → 'Bonjour'). Real perspective, two text nodes, zero JS.
## HTML
```html
<section class="cle-25" aria-label="3D text flip hover demo">
<div class="cle-25__stage">
<div class="cle-25__var"><a href="#a" class="cle-25__flip"><span class="cle-25__box" data-text="Portfolio"><span class="cle-25__f">Portfolio</span></span></a><small class="cle-25__cap">A · whole-word roll</small></div>
<div class="cle-25__var">
<a href="#b" class="cle-25__cascade" aria-label="Showreel"><span class="cle-25__w" aria-hidden="true"><span class="cle-25__box" data-text="S" style="--i:0"><span class="cle-25__f">S</span></span><span class="cle-25__box" data-text="H" style="--i:1"><span class="cle-25__f">H</span></span><span class="cle-25__box" data-text="O" style="--i:2"><span class="cle-25__f">O</span></span><span class="cle-25__box" data-text="W" style="--i:3"><span class="cle-25__f">W</span></span><span class="cle-25__box" data-text="R" style="--i:4"><span class="cle-25__f">R</span></span><span class="cle-25__box" data-text="E" style="--i:5"><span class="cle-25__f">E</span></span><span class="cle-25__box" data-text="E" style="--i:6"><span class="cle-25__f">E</span></span><span class="cle-25__box" data-text="L" style="--i:7"><span class="cle-25__f">L</span></span></span></a>
<small class="cle-25__cap">B · per-letter cascade</small>
</div>
<div class="cle-25__var"><a href="#c" class="cle-25__hinge"><span class="cle-25__h1">Hello</span><span class="cle-25__h2" aria-hidden="true">Bonjour</span></a><small class="cle-25__cap">C · hinge swap</small></div>
</div>
</section>
```
## CSS
```css
.cle-25,
.cle-25 *,
.cle-25 *::before,
.cle-25 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.cle-25 {
width: 100%;
min-height: 100vh;
--ink: oklch(0.93 0.01 80);
--accent: oklch(0.8 0.16 80);
font-family: 'Segoe UI',system-ui,sans-serif;
}
.cle-25__stage {
width: 100%;
height: 100vh;
display: grid;
align-content: center;
justify-items: center;
gap: 52px;
background: linear-gradient(170deg,oklch(0.26 0.02 60),oklch(0.18 0.015 50));
padding: 44px 32px;
}
.cle-25__var {
display: grid;
gap: 15px;
justify-items: center;
}
.cle-25__cap {
font-size: 10.5px;
font-weight: 600;
letter-spacing: .14em;
text-transform: uppercase;
color: oklch(0.6 0.04 70);
}
.cle-25 a {
display: inline-block;
perspective: 600px;
color: var(--ink);
text-decoration: none;
font-size: clamp(28px,4.4vw,40px);
font-weight: 800;
letter-spacing: -.01em;
line-height: 1.1;
}
.cle-25 a:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 8px;
border-radius: 4px;
}
.cle-25__box {
position: relative;
display: inline-block;
transform-style: preserve-3d;
transition: transform .5s cubic-bezier(.35,1.2,.35,1);
}
.cle-25__f {
display: inline-block;
transform: translateZ(.55em);
opacity: 1;
transition: opacity .2s .12s;
}
.cle-25__box::before {
content: attr(data-text);
position: absolute;
inset: 0;
color: var(--accent);
transform: rotateX(90deg) translateZ(.55em);
opacity: 0;
transition: opacity .2s .12s;
}
.cle-25__flip:hover .cle-25__box::before,
.cle-25__flip:focus-visible .cle-25__box::before,
.cle-25__cascade:hover .cle-25__box::before,
.cle-25__cascade:focus-visible .cle-25__box::before {
opacity: 1;
}
.cle-25__flip:hover .cle-25__f,
.cle-25__flip:focus-visible .cle-25__f,
.cle-25__cascade:hover .cle-25__f,
.cle-25__cascade:focus-visible .cle-25__f {
opacity: 0;
}
.cle-25__cascade .cle-25__box::before,
.cle-25__cascade .cle-25__f {
transition-delay: calc(var(--i) * 50ms + 120ms);
}
.cle-25__flip:hover .cle-25__box,
.cle-25__flip:focus-visible .cle-25__box {
transform: rotateX(-90deg);
}
.cle-25__cascade .cle-25__w {
display: inline-block;
letter-spacing: .05em;
}
.cle-25__cascade .cle-25__box {
transition-delay: calc(var(--i) * 50ms);
}
.cle-25__cascade:hover .cle-25__box,
.cle-25__cascade:focus-visible .cle-25__box {
transform: rotateX(-90deg);
}
.cle-25__hinge {
display: inline-grid;
}
.cle-25__hinge > span {
grid-area: 1/1;
display: inline-block;
backface-visibility: hidden;
transition: transform .45s cubic-bezier(.35,1.1,.35,1),opacity .3s;
}
.cle-25__h1 {
transform-origin: 50% 0;
transform: rotateX(0);
}
.cle-25__h2 {
color: var(--accent);
transform-origin: 50% 100%;
transform: rotateX(-90deg);
opacity: 0;
}
.cle-25__hinge:hover .cle-25__h1,
.cle-25__hinge:focus-visible .cle-25__h1 {
transform: rotateX(90deg);
opacity: 0;
}
.cle-25__hinge:hover .cle-25__h2,
.cle-25__hinge:focus-visible .cle-25__h2 {
transform: rotateX(0);
opacity: 1;
}
@media (prefers-reduced-motion: reduce) {
.cle-25 * {
transition-duration: .01s !important;
transition-delay: 0s !important;
}
}
```How this works
The cube illusion needs faces pushed off the rotation axis. The visible label is translated forward by half the line's height; the duplicate (from data-text) is rotated 90° up and pushed the same distance — together they form two faces of a box that pivots around its core:
.box{ transform-style: preserve-3d;
transition: transform .5s cubic-bezier(.35,1.2,.35,1); }
.box .front{ display:inline-block; transform: translateZ(.55em); }
.box::before{ content: attr(data-text); position:absolute; inset:0;
color: var(--accent); transform: rotateX(90deg) translateZ(.55em); }
a:hover .box{ transform: rotateX(-90deg); }The parent link supplies perspective: 600px — without it the rotation flattens into a squish. Everything is in em, so the geometry scales with font-size untouched.
The cascade (B) applies the identical structure to each letter span, staggered by transition-delay: calc(var(--i) * 50ms); the link carries aria-label and the letter spans sit in an aria-hidden wrapper, so assistive tech reads one word. C hinges instead of rolls: the front face folds up from its top edge (transform-origin: top) while a different word unfolds from the bottom — a swap, not a copy.
Make it yours
- Roll depth: the
.55emtranslateZ pair — half your line-height; change both together or faces gap. - Direction:
rotateX(90deg)on the hidden face +rotateX(-90deg)hover = roll up; negate both to roll down. - B's ripple: the 50ms delay step; reverse the
--ivalues for right-to-left. - C's languages: the two spans are plain HTML — pair any strings of similar width.
Gotchas — read before shipping
perspectivegoes on the PARENT of the rotating element; on the element itself it does nothing (that'sperspective()the function, a different tool).- Keep
overflowvisible on the link — clipping decapitates the face mid-roll. Reserve vertical breathing room in the line instead. - Under perspective an edge-on face still projects a thin smear above or below the word — cross-fade each face's
opacityacross the roll's midpoint (here a .2s fade delayed .12s into the .5s roll) so no face ever paints while horizontal. - Duplicate faces must be
aria-hidden/attr()-generated, and the per-letter variant needsaria-labelon the link — split text reads letter-by-letter otherwise.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.2+ | 113+ | 111+ |
preserve-3d, perspective and 3D transforms are Baseline a decade back (Safari long since unprefixed). oklch sets the stated floor.