30 CSS Text Hover Effects05 / 30

CSS onlyMIT licensed

Text Swap / Flip Up Effect

Nav links that swap languages on hover — English slides up and out, Japanese slides in from below, in a clipped two-storey elevator built from one absolute span and translateY. Next to it, the rolodex version: the same swap performed as a real 3D flip, two faces mounted on an invisible prism rotating around rotateX. Both keep exactly one label in the accessibility tree; the decorative twin is aria-hidden, so screen readers hear 'Works', never 'Works 作品'.

Published

Live Demo
Try it

The code

<section class="cth-05" aria-label="Text swap and flip up demo">
  <div class="cth-05__stage">
    <div class="cth-05__scene">
      <header class="cth-05__head">
        <p class="cth-05__eyebrow">Atelier Kōyō &mdash; Kyoto / Melbourne</p>
        <span class="cth-05__chip">EN &harr; JA on hover</span>
      </header>
      <nav class="cth-05__nav" aria-label="Primary">
        <a class="cth-05__swap" href="#cth05-top" id="cth05-top"><span class="cth-05__top">Works</span><span class="cth-05__alt" aria-hidden="true" lang="ja">作品</span></a>
        <a class="cth-05__swap" href="#cth05-top"><span class="cth-05__top">Studio</span><span class="cth-05__alt" aria-hidden="true" lang="ja">工房</span></a>
        <a class="cth-05__swap" href="#cth05-top"><span class="cth-05__top">Journal</span><span class="cth-05__alt" aria-hidden="true" lang="ja">日誌</span></a>
        <a class="cth-05__swap" href="#cth05-top"><span class="cth-05__top">Contact</span><span class="cth-05__alt" aria-hidden="true" lang="ja">連絡</span></a>
      </nav>
      <div class="cth-05__row">
        <a class="cth-05__roll" href="#cth05-top" aria-label="Commissions, open for autumn">
          <span class="cth-05__core" aria-hidden="true">
            <span class="cth-05__face cth-05__face--front">Commissions</span>
            <span class="cth-05__face cth-05__face--alt">Open for autumn</span>
          </span>
        </a>
        <p class="cth-05__label">&larr; the rolodex: a real <code>rotateX</code> prism, not a slide</p>
      </div>
      <p class="cth-05__hint">Hover or <kbd>Tab</kbd>. The Japanese twins are <code>aria-hidden</code> &mdash; screen readers hear one language, not both.</p>
    </div>
  </div>
</section>
.cth-05,
.cth-05 *,
.cth-05 *::before,
.cth-05 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cth-05 {
  --bg: oklch(0.968 0.005 110);
  --ink: oklch(0.23 0.012 130);
  --mut: oklch(0.52 0.015 125);
  --acc: oklch(0.56 0.15 145);
  --line: oklch(0.885 0.01 115);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
}

.cth-05__stage {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  background: var(--bg);
  padding: clamp(20px,4vw,56px);
  container: cth05/inline-size;
}

.cth-05__scene {
  width: min(100%,760px);
  display: flex;
  flex-direction: column;
  gap: clamp(20px,3.4cqi,30px);
}

.cth-05__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  flex-wrap: wrap;
  padding-bottom: 14px;
  border-bottom: 1.5px solid var(--ink);
}

.cth-05__eyebrow {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--mut);
}

.cth-05__chip {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11px;
  color: var(--acc);
  border: 1px solid color-mix(in oklch,var(--acc) 40%,transparent);
  border-radius: 99px;
  padding: 5px 11px;
}

.cth-05__nav {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(6px,1.6cqi,14px);
}

.cth-05__swap {
  position: relative;
  overflow: clip;
  display: inline-block;
  padding: .3em .45em;
  font-size: clamp(26px,5.4cqi,44px);
  font-weight: 800;
  letter-spacing: -.02em;
  line-height: 1.15;
  color: inherit;
  text-decoration: none;
  border-radius: 8px;
}

.cth-05__swap:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 2px;
}

.cth-05__top {
  display: block;
  transition: transform .45s cubic-bezier(.76,0,.24,1);
}

.cth-05__alt {
  position: absolute;
  inset: .3em .45em;
  display: block;
  color: var(--acc);
  transform: translateY(150%);
  transition: transform .45s cubic-bezier(.76,0,.24,1);
}

.cth-05__swap:hover .cth-05__top,
.cth-05__swap:focus-visible .cth-05__top {
  transform: translateY(-150%);
}

.cth-05__swap:hover .cth-05__alt,
.cth-05__swap:focus-visible .cth-05__alt {
  transform: translateY(0);
}

.cth-05__row {
  display: flex;
  align-items: center;
  gap: clamp(14px,3cqi,24px);
  flex-wrap: wrap;
}

.cth-05__roll {
  display: inline-block;
  perspective: 900px;
  text-decoration: none;
  border-radius: 10px;
}

.cth-05__roll:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 4px;
}

.cth-05__core {
  position: relative;
  display: block;
  transform-style: preserve-3d;
  transition: transform .55s cubic-bezier(.68,-0.15,.27,1.15);
  font-size: clamp(19px,3cqi,25px);
  font-weight: 750;
  line-height: 1;
  height: 1em;
}

.cth-05__face {
  display: flex;
  align-items: center;
  height: 1em;
  white-space: nowrap;
  backface-visibility: hidden;
}

.cth-05__face--front {
  color: var(--ink);
  transform: translateZ(.52em);
}

.cth-05__face--alt {
  position: absolute;
  inset: 0;
  color: var(--acc);
  transform: rotateX(90deg) translateZ(.52em);
}

.cth-05__roll:hover .cth-05__core,
.cth-05__roll:focus-visible .cth-05__core {
  transform: rotateX(-90deg);
}

.cth-05__label {
  font-size: 13px;
  color: var(--mut);
}

.cth-05__label code,
.cth-05__hint code {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: .9em;
  background: oklch(0.92 0.008 115);
  padding: 1px 6px;
  border-radius: 4px;
}

.cth-05__hint {
  font-size: 12.5px;
  color: var(--mut);
}

.cth-05__hint kbd {
  font-family: ui-monospace,Menlo,monospace;
  font-size: 11px;
  border: 1px solid var(--line);
  border-bottom-width: 2px;
  border-radius: 5px;
  padding: 2px 6px;
  background: oklch(1 0 0);
}

@media (prefers-reduced-motion: reduce) {
  .cth-05 * {
    transition-duration: .01ms !important;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Text Hover 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: Text Swap / Flip Up Effect
Source: https://codefronts.com/motion/css-text-hover-effects/text-swap-flip-up-effect/

Nav links that swap languages on hover — English slides up and out, Japanese slides in from below, in a clipped two-storey elevator built from one absolute span and translateY. Next to it, the rolodex version: the same swap performed as a real 3D flip, two faces mounted on an invisible prism rotating around rotateX. Both keep exactly one label in the accessibility tree; the decorative twin is aria-hidden, so screen readers hear 'Works', never 'Works 作品'.
## HTML
```html
<section class="cth-05" aria-label="Text swap and flip up demo">
  <div class="cth-05__stage">
    <div class="cth-05__scene">
      <header class="cth-05__head">
        <p class="cth-05__eyebrow">Atelier Kōyō &mdash; Kyoto / Melbourne</p>
        <span class="cth-05__chip">EN &harr; JA on hover</span>
      </header>
      <nav class="cth-05__nav" aria-label="Primary">
        <a class="cth-05__swap" href="#cth05-top" id="cth05-top"><span class="cth-05__top">Works</span><span class="cth-05__alt" aria-hidden="true" lang="ja">作品</span></a>
        <a class="cth-05__swap" href="#cth05-top"><span class="cth-05__top">Studio</span><span class="cth-05__alt" aria-hidden="true" lang="ja">工房</span></a>
        <a class="cth-05__swap" href="#cth05-top"><span class="cth-05__top">Journal</span><span class="cth-05__alt" aria-hidden="true" lang="ja">日誌</span></a>
        <a class="cth-05__swap" href="#cth05-top"><span class="cth-05__top">Contact</span><span class="cth-05__alt" aria-hidden="true" lang="ja">連絡</span></a>
      </nav>
      <div class="cth-05__row">
        <a class="cth-05__roll" href="#cth05-top" aria-label="Commissions, open for autumn">
          <span class="cth-05__core" aria-hidden="true">
            <span class="cth-05__face cth-05__face--front">Commissions</span>
            <span class="cth-05__face cth-05__face--alt">Open for autumn</span>
          </span>
        </a>
        <p class="cth-05__label">&larr; the rolodex: a real <code>rotateX</code> prism, not a slide</p>
      </div>
      <p class="cth-05__hint">Hover or <kbd>Tab</kbd>. The Japanese twins are <code>aria-hidden</code> &mdash; screen readers hear one language, not both.</p>
    </div>
  </div>
</section>
```
## CSS
```css
.cth-05,
.cth-05 *,
.cth-05 *::before,
.cth-05 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.cth-05 {
  --bg: oklch(0.968 0.005 110);
  --ink: oklch(0.23 0.012 130);
  --mut: oklch(0.52 0.015 125);
  --acc: oklch(0.56 0.15 145);
  --line: oklch(0.885 0.01 115);
  font-family: 'Segoe UI',system-ui,sans-serif;
  color: var(--ink);
}

.cth-05__stage {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  background: var(--bg);
  padding: clamp(20px,4vw,56px);
  container: cth05/inline-size;
}

.cth-05__scene {
  width: min(100%,760px);
  display: flex;
  flex-direction: column;
  gap: clamp(20px,3.4cqi,30px);
}

.cth-05__head {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 14px;
  flex-wrap: wrap;
  padding-bottom: 14px;
  border-bottom: 1.5px solid var(--ink);
}

.cth-05__eyebrow {
  font-size: 12px;
  font-weight: 700;
  letter-spacing: .22em;
  text-transform: uppercase;
  color: var(--mut);
}

.cth-05__chip {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11px;
  color: var(--acc);
  border: 1px solid color-mix(in oklch,var(--acc) 40%,transparent);
  border-radius: 99px;
  padding: 5px 11px;
}

.cth-05__nav {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(6px,1.6cqi,14px);
}

.cth-05__swap {
  position: relative;
  overflow: clip;
  display: inline-block;
  padding: .3em .45em;
  font-size: clamp(26px,5.4cqi,44px);
  font-weight: 800;
  letter-spacing: -.02em;
  line-height: 1.15;
  color: inherit;
  text-decoration: none;
  border-radius: 8px;
}

.cth-05__swap:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 2px;
}

.cth-05__top {
  display: block;
  transition: transform .45s cubic-bezier(.76,0,.24,1);
}

.cth-05__alt {
  position: absolute;
  inset: .3em .45em;
  display: block;
  color: var(--acc);
  transform: translateY(150%);
  transition: transform .45s cubic-bezier(.76,0,.24,1);
}

.cth-05__swap:hover .cth-05__top,
.cth-05__swap:focus-visible .cth-05__top {
  transform: translateY(-150%);
}

.cth-05__swap:hover .cth-05__alt,
.cth-05__swap:focus-visible .cth-05__alt {
  transform: translateY(0);
}

.cth-05__row {
  display: flex;
  align-items: center;
  gap: clamp(14px,3cqi,24px);
  flex-wrap: wrap;
}

.cth-05__roll {
  display: inline-block;
  perspective: 900px;
  text-decoration: none;
  border-radius: 10px;
}

.cth-05__roll:focus-visible {
  outline: 2px solid var(--acc);
  outline-offset: 4px;
}

.cth-05__core {
  position: relative;
  display: block;
  transform-style: preserve-3d;
  transition: transform .55s cubic-bezier(.68,-0.15,.27,1.15);
  font-size: clamp(19px,3cqi,25px);
  font-weight: 750;
  line-height: 1;
  height: 1em;
}

.cth-05__face {
  display: flex;
  align-items: center;
  height: 1em;
  white-space: nowrap;
  backface-visibility: hidden;
}

.cth-05__face--front {
  color: var(--ink);
  transform: translateZ(.52em);
}

.cth-05__face--alt {
  position: absolute;
  inset: 0;
  color: var(--acc);
  transform: rotateX(90deg) translateZ(.52em);
}

.cth-05__roll:hover .cth-05__core,
.cth-05__roll:focus-visible .cth-05__core {
  transform: rotateX(-90deg);
}

.cth-05__label {
  font-size: 13px;
  color: var(--mut);
}

.cth-05__label code,
.cth-05__hint code {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: .9em;
  background: oklch(0.92 0.008 115);
  padding: 1px 6px;
  border-radius: 4px;
}

.cth-05__hint {
  font-size: 12.5px;
  color: var(--mut);
}

.cth-05__hint kbd {
  font-family: ui-monospace,Menlo,monospace;
  font-size: 11px;
  border: 1px solid var(--line);
  border-bottom-width: 2px;
  border-radius: 5px;
  padding: 2px 6px;
  background: oklch(1 0 0);
}

@media (prefers-reduced-motion: reduce) {
  .cth-05 * {
    transition-duration: .01ms !important;
  }
}
```

How this works

The slide swap is a one-span elevator inside a clipped link. The visible label is normal flow; its twin hangs one storey below at translateY(100%); hover moves both up one storey:

.swap{ position:relative; overflow:clip; }
.swap .top{ display:block;
  transition:transform .45s cubic-bezier(.76,0,.24,1); }
.swap .alt{ position:absolute; inset:0;
  transform:translateY(100%);        /* parked downstairs */
  transition:inherit; }
.swap:hover .top{ transform:translateY(-100%); }
.swap:hover .alt{ transform:translateY(0); }

Percentages here are self-measuring — 100% of the SPAN's own height — so the same three rules fit a 13px breadcrumb or a 40px menu item. overflow:clip (cheaper than hidden, can't be scrolled by a stray focus) guillotines the storeys, and the shared cubic-bezier(.76,0,.24,1) gives the departure a heavy start and soft landing, which is what makes it feel mechanical rather than floaty.

The rolodex variant trades the elevator for a prism: both faces sit on a transform-style:preserve-3d core, the front pushed out translateZ(.52em), the alt face pre-rotated rotateX(90deg) onto the top surface. Hovering rotates the whole core rotateX(-90deg), and perspective on the PARENT (never the core — it would flatten the children) sells the turn. The .52em ≈ half the line-height in em, which is why the geometry survives font-size changes untouched.

Make it yours

  • Swap anything, not just translations: 'Download' → file size, a price → 'Add to cart', a nav label → its keyboard shortcut. The alt span is free content.
  • Direction grammar: translateY reads as refresh; translateX(-100%) on the pair reads as next/previous; pick one axis per surface and keep it.
  • Stagger a menu: transition-delay:calc(var(--i)*35ms) on each link turns a bar of swaps into a wave when a parent :hover triggers them together.
  • For the rolodex, tint the alt face slightly darker (color-mix 85% ink) — the fake shading reads as light falling on a turning surface.

Gotchas — read before shipping

  • The absolute twin sizes itself to the LINK's box — if the alt label is longer than the primary, it wraps or clips. Keep pairs similar length, or set min-width to the longer word (ch units work well).
  • Percentage translateY needs the span to be display:block — inline spans ignore transforms entirely.
  • perspective belongs on the parent of the rotating element; perspective ON the rotating element itself is a no-op for its own rotation (it only affects ITS children).
  • Whitespace inside the link becomes a stray text node that screen readers may voice as a pause — keep the two spans snug against the tags.

Browser support

ChromeSafariFirefoxEdge
111+16.4+113+111+

Transforms and preserve-3d are 2015-era; overflow:clip is Chrome 90+/Safari 16+/Firefox 81+ (use overflow:hidden below that). Floor reflects oklch() tokens — swap hex to go older.

Techniques used in this demo

Search CodeFronts

Loading…