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.

Pure CSS MIT licensed
Live Demo Open in tab
Open in playground

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>
.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 rotateX with rotateY and adjusting the translateX offsets accordingly.
  • Increase the 3D depth by raising the perspective value from 600px to 1200px — smaller values create a more dramatic foreshortening effect.
  • Add a colour change on reveal by transitioning the button background simultaneously with the flip to reinforce the state change visually.
  • Show an icon in the back label by replacing the back span text 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__btn an increasing animation-delay when a parent class triggers the group flip via JS.

Watch out for

  • backface-visibility: hidden is 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: relative button element can occasionally produce a rendering artefact where the flip stalls — wrapping the spans in a dedicated transform-style: preserve-3d container 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-width on the button or use width: max-content.

Browser support

ChromeSafariFirefoxEdge
36+ 9+ 16+ 36+

CSS 3D transforms with backface-visibility are well-supported; no prefixes needed in modern browsers.

Search CodeFronts

Loading…