Pure TailwindMIT licensed

Tailwind Split Text Hover Effect

Four hovers that cut a word in half and push the pieces apart — top/bottom, left/right, a diagonal polygon and a split-plus-fade. Two clipped copies of the text live in before: and after:; the base element goes transparent so nothing shows through the seam.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

The code

<section class="thv-05 w-full min-h-svh grid place-items-center bg-thv05-bg font-thv05-sans px-6 py-14">
  <div class="w-[min(820px,100%)] grid grid-cols-[repeat(auto-fit,minmax(260px,1fr))] gap-10">
    <div class="flex flex-col items-center gap-6 px-8 py-14 rounded-2xl bg-white/[0.025] border border-white/[0.07]">
      <span tabindex="0" data-text="DIVIDE" class="relative inline-block overflow-hidden text-[clamp(2rem,6vw,3rem)] font-black tracking-[0.1em] text-transparent rounded-xs focus-visible:outline-2 focus-visible:outline-offset-6 focus-visible:outline-thv05-cyan before:content-[attr(data-text)] before:absolute before:inset-0 before:text-thv05-cyan before:[clip-path:inset(0_0_50%_0)] before:transition-transform before:duration-450 before:ease-[cubic-bezier(.4,0,.2,1)] after:content-[attr(data-text)] after:absolute after:inset-0 after:text-thv05-cyan after:[clip-path:inset(50%_0_0_0)] after:transition-transform after:duration-450 after:ease-[cubic-bezier(.4,0,.2,1)] hover:before:-translate-y-[0.55em] hover:after:translate-y-[0.55em] focus-visible:before:-translate-y-[0.55em] focus-visible:after:translate-y-[0.55em] motion-reduce:before:transition-none motion-reduce:after:transition-none">DIVIDE</span>
      <span class="text-[11px] tracking-[0.15em] uppercase text-thv05-dim">top / bottom split</span>
    </div>
    <div class="flex flex-col items-center gap-6 px-8 py-14 rounded-2xl bg-white/[0.025] border border-white/[0.07]">
      <span tabindex="0" data-text="SCHISM" class="relative inline-block overflow-hidden text-[clamp(2rem,6vw,3rem)] font-black tracking-[0.1em] text-transparent rounded-xs focus-visible:outline-2 focus-visible:outline-offset-6 focus-visible:outline-thv05-rose before:content-[attr(data-text)] before:absolute before:inset-0 before:text-thv05-rose before:[clip-path:inset(0_50%_0_0)] before:transition-transform before:duration-450 before:ease-[cubic-bezier(.4,0,.2,1)] after:content-[attr(data-text)] after:absolute after:inset-0 after:text-thv05-amber after:[clip-path:inset(0_0_0_50%)] after:transition-transform after:duration-450 after:ease-[cubic-bezier(.4,0,.2,1)] hover:before:-translate-x-[0.4em] hover:after:translate-x-[0.4em] focus-visible:before:-translate-x-[0.4em] focus-visible:after:translate-x-[0.4em] motion-reduce:before:transition-none motion-reduce:after:transition-none">SCHISM</span>
      <span class="text-[11px] tracking-[0.15em] uppercase text-thv05-dim">left / right push</span>
    </div>
    <div class="flex flex-col items-center gap-6 px-8 py-14 rounded-2xl bg-white/[0.025] border border-white/[0.07]">
      <span tabindex="0" data-text="SLASH" class="relative inline-block text-[clamp(2rem,6vw,3rem)] font-black tracking-[0.1em] text-transparent rounded-xs focus-visible:outline-2 focus-visible:outline-offset-6 focus-visible:outline-thv05-violet before:content-[attr(data-text)] before:absolute before:inset-0 before:text-thv05-violet before:[clip-path:polygon(0_0,100%_0,60%_100%,0_100%)] before:transition-transform before:duration-450 before:ease-[cubic-bezier(.4,0,.2,1)] after:content-[attr(data-text)] after:absolute after:inset-0 after:text-thv05-cyan after:[clip-path:polygon(60%_0,100%_0,100%_100%,0_100%)] after:transition-transform after:duration-450 after:ease-[cubic-bezier(.4,0,.2,1)] hover:before:-translate-x-[0.3em] hover:before:-translate-y-[0.3em] hover:after:translate-x-[0.3em] hover:after:translate-y-[0.3em] focus-visible:before:-translate-x-[0.3em] focus-visible:before:-translate-y-[0.3em] focus-visible:after:translate-x-[0.3em] focus-visible:after:translate-y-[0.3em] motion-reduce:before:transition-none motion-reduce:after:transition-none">SLASH</span>
      <span class="text-[11px] tracking-[0.15em] uppercase text-thv05-dim">diagonal polygon</span>
    </div>
    <div class="flex flex-col items-center gap-6 px-8 py-14 rounded-2xl bg-white/[0.025] border border-white/[0.07]">
      <span tabindex="0" data-text="RIFT" class="relative inline-block text-[clamp(2rem,6vw,3rem)] font-black tracking-[0.1em] text-transparent rounded-xs focus-visible:outline-2 focus-visible:outline-offset-6 focus-visible:outline-thv05-amber before:content-[attr(data-text)] before:absolute before:inset-0 before:text-thv05-amber before:[clip-path:inset(0_0_50%_0)] before:transition-[transform,opacity] before:duration-450 before:ease-[cubic-bezier(.4,0,.2,1)] after:content-[attr(data-text)] after:absolute after:inset-0 after:text-thv05-rose after:[clip-path:inset(50%_0_0_0)] after:transition-[transform,opacity] after:duration-450 after:ease-[cubic-bezier(.4,0,.2,1)] hover:before:-translate-y-[0.6em] hover:before:-rotate-3 hover:before:opacity-0 hover:after:translate-y-[0.6em] hover:after:rotate-3 hover:after:opacity-0 focus-visible:before:-translate-y-[0.6em] focus-visible:before:opacity-0 focus-visible:after:translate-y-[0.6em] focus-visible:after:opacity-0 motion-reduce:before:transition-none motion-reduce:after:transition-none">RIFT</span>
      <span class="text-[11px] tracking-[0.15em] uppercase text-thv05-dim">split + fade</span>
    </div>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-thv05-bg: #020917;
  --color-thv05-bg: oklch(0.13 0.03 260);
  --color-thv05-dim: #475569;
  --color-thv05-dim: oklch(0.45 0.03 257);
  --color-thv05-cyan: #06b6d4;
  --color-thv05-cyan: oklch(0.71 0.14 208);
  --color-thv05-rose: #f43f5e;
  --color-thv05-rose: oklch(0.65 0.23 16);
  --color-thv05-violet: #8b5cf6;
  --color-thv05-violet: oklch(0.63 0.21 295);
  --color-thv05-amber: #f59e0b;
  --color-thv05-amber: oklch(0.77 0.16 70);
  --font-thv05-sans: 'Space Grotesk', system-ui, sans-serif;
}

.thv-05 {
  width: 100%;
  min-height: 100svh;
}

.thv-05 ::selection {
  background: var(--color-thv05-cyan);
  color: #fff;
}

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

Compiles in the browser — perfect for a CodePen or a quick test, not for production.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-thv05-bg: #020917;
  --color-thv05-bg: oklch(0.13 0.03 260);
  --color-thv05-dim: #475569;
  --color-thv05-dim: oklch(0.45 0.03 257);
  --color-thv05-cyan: #06b6d4;
  --color-thv05-cyan: oklch(0.71 0.14 208);
  --color-thv05-rose: #f43f5e;
  --color-thv05-rose: oklch(0.65 0.23 16);
  --color-thv05-violet: #8b5cf6;
  --color-thv05-violet: oklch(0.63 0.21 295);
  --color-thv05-amber: #f59e0b;
  --color-thv05-amber: oklch(0.77 0.16 70);
  --font-thv05-sans: 'Space Grotesk', system-ui, sans-serif;
}

.thv-05 {
  width: 100%;
  min-height: 100svh;
}

.thv-05 ::selection {
  background: var(--color-thv05-cyan);
  color: #fff;
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;800&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • Sixteen CSS rules become four class lists. The clip and the translate that depends on it are now two tokens apart instead of twenty lines.
  • UI change from the CSS original: the words are focusable and split on focus-visible:; the fade variant drops the rotation under keyboard focus so the word does not leave the ring.
  • Em-based arbitrary translates (-translate-y-[0.55em]) are the detail that makes the port responsive — the source's .55em was already right, and Tailwind carries it verbatim.
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 demo 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: Tailwind Split Text Hover Effect
Source: https://codefronts.com/motion/tailwind-css-hover-effects/tailwind-split-text-hover-effect/

Four hovers that cut a word in half and push the pieces apart — top/bottom, left/right, a diagonal polygon and a split-plus-fade. Two clipped copies of the text live in before: and after:; the base element goes transparent so nothing shows through the seam.
## HTML
```html
<section class="thv-05 w-full min-h-svh grid place-items-center bg-thv05-bg font-thv05-sans px-6 py-14">
  <div class="w-[min(820px,100%)] grid grid-cols-[repeat(auto-fit,minmax(260px,1fr))] gap-10">
    <div class="flex flex-col items-center gap-6 px-8 py-14 rounded-2xl bg-white/[0.025] border border-white/[0.07]">
      <span tabindex="0" data-text="DIVIDE" class="relative inline-block overflow-hidden text-[clamp(2rem,6vw,3rem)] font-black tracking-[0.1em] text-transparent rounded-xs focus-visible:outline-2 focus-visible:outline-offset-6 focus-visible:outline-thv05-cyan before:content-[attr(data-text)] before:absolute before:inset-0 before:text-thv05-cyan before:[clip-path:inset(0_0_50%_0)] before:transition-transform before:duration-450 before:ease-[cubic-bezier(.4,0,.2,1)] after:content-[attr(data-text)] after:absolute after:inset-0 after:text-thv05-cyan after:[clip-path:inset(50%_0_0_0)] after:transition-transform after:duration-450 after:ease-[cubic-bezier(.4,0,.2,1)] hover:before:-translate-y-[0.55em] hover:after:translate-y-[0.55em] focus-visible:before:-translate-y-[0.55em] focus-visible:after:translate-y-[0.55em] motion-reduce:before:transition-none motion-reduce:after:transition-none">DIVIDE</span>
      <span class="text-[11px] tracking-[0.15em] uppercase text-thv05-dim">top / bottom split</span>
    </div>
    <div class="flex flex-col items-center gap-6 px-8 py-14 rounded-2xl bg-white/[0.025] border border-white/[0.07]">
      <span tabindex="0" data-text="SCHISM" class="relative inline-block overflow-hidden text-[clamp(2rem,6vw,3rem)] font-black tracking-[0.1em] text-transparent rounded-xs focus-visible:outline-2 focus-visible:outline-offset-6 focus-visible:outline-thv05-rose before:content-[attr(data-text)] before:absolute before:inset-0 before:text-thv05-rose before:[clip-path:inset(0_50%_0_0)] before:transition-transform before:duration-450 before:ease-[cubic-bezier(.4,0,.2,1)] after:content-[attr(data-text)] after:absolute after:inset-0 after:text-thv05-amber after:[clip-path:inset(0_0_0_50%)] after:transition-transform after:duration-450 after:ease-[cubic-bezier(.4,0,.2,1)] hover:before:-translate-x-[0.4em] hover:after:translate-x-[0.4em] focus-visible:before:-translate-x-[0.4em] focus-visible:after:translate-x-[0.4em] motion-reduce:before:transition-none motion-reduce:after:transition-none">SCHISM</span>
      <span class="text-[11px] tracking-[0.15em] uppercase text-thv05-dim">left / right push</span>
    </div>
    <div class="flex flex-col items-center gap-6 px-8 py-14 rounded-2xl bg-white/[0.025] border border-white/[0.07]">
      <span tabindex="0" data-text="SLASH" class="relative inline-block text-[clamp(2rem,6vw,3rem)] font-black tracking-[0.1em] text-transparent rounded-xs focus-visible:outline-2 focus-visible:outline-offset-6 focus-visible:outline-thv05-violet before:content-[attr(data-text)] before:absolute before:inset-0 before:text-thv05-violet before:[clip-path:polygon(0_0,100%_0,60%_100%,0_100%)] before:transition-transform before:duration-450 before:ease-[cubic-bezier(.4,0,.2,1)] after:content-[attr(data-text)] after:absolute after:inset-0 after:text-thv05-cyan after:[clip-path:polygon(60%_0,100%_0,100%_100%,0_100%)] after:transition-transform after:duration-450 after:ease-[cubic-bezier(.4,0,.2,1)] hover:before:-translate-x-[0.3em] hover:before:-translate-y-[0.3em] hover:after:translate-x-[0.3em] hover:after:translate-y-[0.3em] focus-visible:before:-translate-x-[0.3em] focus-visible:before:-translate-y-[0.3em] focus-visible:after:translate-x-[0.3em] focus-visible:after:translate-y-[0.3em] motion-reduce:before:transition-none motion-reduce:after:transition-none">SLASH</span>
      <span class="text-[11px] tracking-[0.15em] uppercase text-thv05-dim">diagonal polygon</span>
    </div>
    <div class="flex flex-col items-center gap-6 px-8 py-14 rounded-2xl bg-white/[0.025] border border-white/[0.07]">
      <span tabindex="0" data-text="RIFT" class="relative inline-block text-[clamp(2rem,6vw,3rem)] font-black tracking-[0.1em] text-transparent rounded-xs focus-visible:outline-2 focus-visible:outline-offset-6 focus-visible:outline-thv05-amber before:content-[attr(data-text)] before:absolute before:inset-0 before:text-thv05-amber before:[clip-path:inset(0_0_50%_0)] before:transition-[transform,opacity] before:duration-450 before:ease-[cubic-bezier(.4,0,.2,1)] after:content-[attr(data-text)] after:absolute after:inset-0 after:text-thv05-rose after:[clip-path:inset(50%_0_0_0)] after:transition-[transform,opacity] after:duration-450 after:ease-[cubic-bezier(.4,0,.2,1)] hover:before:-translate-y-[0.6em] hover:before:-rotate-3 hover:before:opacity-0 hover:after:translate-y-[0.6em] hover:after:rotate-3 hover:after:opacity-0 focus-visible:before:-translate-y-[0.6em] focus-visible:before:opacity-0 focus-visible:after:translate-y-[0.6em] focus-visible:after:opacity-0 motion-reduce:before:transition-none motion-reduce:after:transition-none">RIFT</span>
      <span class="text-[11px] tracking-[0.15em] uppercase text-thv05-dim">split + fade</span>
    </div>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-thv05-bg: #020917;
  --color-thv05-bg: oklch(0.13 0.03 260);
  --color-thv05-dim: #475569;
  --color-thv05-dim: oklch(0.45 0.03 257);
  --color-thv05-cyan: #06b6d4;
  --color-thv05-cyan: oklch(0.71 0.14 208);
  --color-thv05-rose: #f43f5e;
  --color-thv05-rose: oklch(0.65 0.23 16);
  --color-thv05-violet: #8b5cf6;
  --color-thv05-violet: oklch(0.63 0.21 295);
  --color-thv05-amber: #f59e0b;
  --color-thv05-amber: oklch(0.77 0.16 70);
  --font-thv05-sans: 'Space Grotesk', system-ui, sans-serif;
}

.thv-05 {
  width: 100%;
  min-height: 100svh;
}

.thv-05 ::selection {
  background: var(--color-thv05-cyan);
  color: #fff;
}
```

How this works

Two copies, two clips, one class list. Both pseudo-elements read content-[attr(data-text)] and sit at absolute inset-0; the only difference between them is the clip and the hover translate. In CSS that was four rules per variant, two of them far below the others.

Em translates keep the split proportional. hover:before:-translate-y-[0.55em] moves by a share of the font size, so the effect survives the clamp() type scale unchanged — a px translate would look right at one size only.

The base text stays readable to machines. text-transparent hides the original glyphs behind the two halves without removing them, so assistive technology still announces the word once, not twice, and never announces the pseudo-element copies.

Make it yours

  • Deeper split: hover:before:-translate-y-[1.2em] and its mirror.
  • Two-tone the halves by giving before:text-* and after:text-* different tokens (the left/right variant already does).
  • Steepen the diagonal by editing the polygon's middle stops.

Gotchas — read before shipping

  • Keep overflow-hidden on the word if the halves should disappear at the boundary rather than float over the card.
  • Without text-transparent on the host, the un-split original shows through the gap.
  • clip-path and will-change:transform on the same element can defeat the clip in some engines — don't add one here.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

clip-path: inset() and polygon() are Baseline; the floor is Tailwind v4's generated sheet.

Techniques used in this demo

Utilitiesfirst-class utility or variant

Arbitraryneeds bracket syntax

Search CodeFronts

Loading…