20 CSS Animated Buttons 10 / 20
CSS Text Flip Button Hover
Button labels flip vertically on hover to reveal a secondary message — built with CSS 3D perspective transforms on stacked spans, no JavaScript required.
The code
<div class="ab-10">
<p class="ab-10__label">Hover to flip text</p>
<div class="ab-10__row">
<button class="ab-10__btn ab-10__btn--indigo">
<span class="ab-10__inner">
<span class="ab-10__front">View Pricing</span>
<span class="ab-10__back">From $9/mo →</span>
</span>
</button>
<button class="ab-10__btn ab-10__btn--teal">
<span class="ab-10__inner">
<span class="ab-10__front">Follow Us</span>
<span class="ab-10__back">@codefronts</span>
</span>
</button>
<button class="ab-10__btn ab-10__btn--rose">
<span class="ab-10__inner">
<span class="ab-10__front">Join Beta</span>
<span class="ab-10__back">Spots Left: 3</span>
</span>
</button>
</div>
</div> <div class="ab-10">
<p class="ab-10__label">Hover to flip text</p>
<div class="ab-10__row">
<button class="ab-10__btn ab-10__btn--indigo">
<span class="ab-10__inner">
<span class="ab-10__front">View Pricing</span>
<span class="ab-10__back">From $9/mo →</span>
</span>
</button>
<button class="ab-10__btn ab-10__btn--teal">
<span class="ab-10__inner">
<span class="ab-10__front">Follow Us</span>
<span class="ab-10__back">@codefronts</span>
</span>
</button>
<button class="ab-10__btn ab-10__btn--rose">
<span class="ab-10__inner">
<span class="ab-10__front">Join Beta</span>
<span class="ab-10__back">Spots Left: 3</span>
</span>
</button>
</div>
</div>.ab-10,.ab-10 *,.ab-10 *::before,.ab-10 *::after{box-sizing:border-box;margin:0;padding:0}
.ab-10 ::selection{background:#4f46e5;color:#fff}
.ab-10{
font-family:system-ui,sans-serif;
background:#0f0c1f;
min-height:100vh;
display:flex;
flex-direction:column;
align-items:center;
justify-content:center;
gap:2rem;
padding:2rem;
}
.ab-10__label{
font-size:.78rem;
letter-spacing:.14em;
text-transform:uppercase;
color:#818cf8;
opacity:.6;
}
.ab-10__row{
display:flex;
gap:1.25rem;
flex-wrap:wrap;
justify-content:center;
}
.ab-10__btn{
--c:#4f46e5;
padding:0;
background:var(--c);
border:none;
border-radius:10px;
cursor:pointer;
outline:none;
perspective:600px;
}
.ab-10__btn--indigo{--c:#4f46e5}
.ab-10__btn--teal{--c:#0d9488}
.ab-10__btn--rose{--c:#e11d48}
.ab-10__inner{
position:relative;
display:block;
padding:.85rem 2rem;
width:9rem;
height:3.1rem;
overflow:hidden;
transform-style:preserve-3d;
}
.ab-10__front,.ab-10__back{
position:absolute;
inset:0;
display:flex;
align-items:center;
justify-content:center;
font-size:.95rem;
font-weight:700;
color:#fff;
letter-spacing:.04em;
white-space:nowrap;
backface-visibility:hidden;
transition:transform .35s cubic-bezier(.4,0,.2,1);
}
.ab-10__front{transform:translateY(0) rotateX(0)}
.ab-10__back{transform:translateY(100%) rotateX(-90deg)}
.ab-10__btn:hover .ab-10__front{transform:translateY(-100%) rotateX(90deg)}
.ab-10__btn:hover .ab-10__back{transform:translateY(0) rotateX(0)}
@media(prefers-reduced-motion:reduce){
.ab-10__front,.ab-10__back{transition:none}
.ab-10__btn:hover .ab-10__front{transform:none;opacity:0}
.ab-10__btn:hover .ab-10__back{transform:none;opacity:1}
} .ab-10,.ab-10 *,.ab-10 *::before,.ab-10 *::after{box-sizing:border-box;margin:0;padding:0}
.ab-10 ::selection{background:#4f46e5;color:#fff}
.ab-10{
font-family:system-ui,sans-serif;
background:#0f0c1f;
min-height:100vh;
display:flex;
flex-direction:column;
align-items:center;
justify-content:center;
gap:2rem;
padding:2rem;
}
.ab-10__label{
font-size:.78rem;
letter-spacing:.14em;
text-transform:uppercase;
color:#818cf8;
opacity:.6;
}
.ab-10__row{
display:flex;
gap:1.25rem;
flex-wrap:wrap;
justify-content:center;
}
.ab-10__btn{
--c:#4f46e5;
padding:0;
background:var(--c);
border:none;
border-radius:10px;
cursor:pointer;
outline:none;
perspective:600px;
}
.ab-10__btn--indigo{--c:#4f46e5}
.ab-10__btn--teal{--c:#0d9488}
.ab-10__btn--rose{--c:#e11d48}
.ab-10__inner{
position:relative;
display:block;
padding:.85rem 2rem;
width:9rem;
height:3.1rem;
overflow:hidden;
transform-style:preserve-3d;
}
.ab-10__front,.ab-10__back{
position:absolute;
inset:0;
display:flex;
align-items:center;
justify-content:center;
font-size:.95rem;
font-weight:700;
color:#fff;
letter-spacing:.04em;
white-space:nowrap;
backface-visibility:hidden;
transition:transform .35s cubic-bezier(.4,0,.2,1);
}
.ab-10__front{transform:translateY(0) rotateX(0)}
.ab-10__back{transform:translateY(100%) rotateX(-90deg)}
.ab-10__btn:hover .ab-10__front{transform:translateY(-100%) rotateX(90deg)}
.ab-10__btn:hover .ab-10__back{transform:translateY(0) rotateX(0)}
@media(prefers-reduced-motion:reduce){
.ab-10__front,.ab-10__back{transition:none}
.ab-10__btn:hover .ab-10__front{transform:none;opacity:0}
.ab-10__btn:hover .ab-10__back{transform:none;opacity:1}
}How this works
Each button contains two span elements — .front and .back — stacked inside a .ab-10__inner container that has perspective: 600px. The container has a fixed height equal to one label line. In the resting state, .back is hidden beneath by transform: rotateX(90deg) and translated to sit just below the viewport of the container (via translateY(100%)). On :hover, .front rotates to rotateX(-90deg) (flipping upward out of view) while .back rotates back to 0deg, completing the vertical flip. Both transitions use the same duration and easing.
A transition-delay of 0s on the outgoing element and a matching delay on the incoming ensures the two halves of the flip feel like a single synchronized pivot. backface-visibility: hidden prevents the ghost of each panel showing through the other during mid-rotation. All transforms happen on the compositor, making the effect entirely jank-free.
Customize
- Change the flip axis from vertical to horizontal by replacing
rotateXwithrotateYand adjusting thetranslateXoffsets accordingly. - Increase the 3D depth by raising the
perspectivevalue from600pxto1200px— smaller values create a more dramatic foreshortening effect. - Add a colour change on reveal by transitioning the button
backgroundsimultaneously with the flip to reinforce the state change visually. - Show an icon in the back label by replacing the back
spantext with an SVG inline icon alongside the text for a "click to confirm" interaction. - Stagger multiple flipping buttons in a row by giving each
.ab-10__btnan increasinganimation-delaywhen a parent class triggers the group flip via JS.
Watch out for
backface-visibility: hiddenis required — without it some browsers render a mirror image of the front panel visible through the back during the rotation.- On Safari, 3D transforms inside a
position: relativebutton element can occasionally produce a rendering artefact where the flip stalls — wrapping the spans in a dedicatedtransform-style: preserve-3dcontainer resolves this. - Very short labels (< 3 characters) may not fill the button width after the flip if the back label is longer — fix by setting an explicit
min-widthon the button or usewidth: max-content.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 36+ | 9+ | 16+ | 36+ |
CSS 3D transforms with backface-visibility are well-supported; no prefixes needed in modern browsers.