30 Tailwind CSS Text Hover Effects
09 / 30
Pure TailwindMIT licensed

Tailwind 3D Text Pop and Lift Hover Effect

A festival headline that pops out of the page: six same-length text-shadow layers interpolate from 0 to an extruded side, with active: pressing it back in on click. Matching neo-brutalist buttons lift and press with box-shadow. Everything rides a springy cubic-bezier(.34,1.8,.4,1).

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="tth-09 w-full min-h-svh grid place-items-center bg-tth09-bg bg-[radial-gradient(90%_70%_at_50%_20%,var(--color-tth09-glow),var(--color-tth09-bg)_75%)] px-5 py-14 font-tth09-sans text-tth09-ink" aria-label="3D pop lift text demo">
  <div class="flex flex-col items-center gap-[clamp(16px,3vw,24px)] text-center">
    <p class="text-[12.5px] font-extrabold uppercase tracking-[0.24em] text-tth09-mut">Riverside park &middot; Jan 30&ndash;31 &middot; all ages</p>
    <h2 class="leading-[0.96]"><a id="tth09-top" href="#tth09-top" class="inline-block rounded-md text-[clamp(58px,13vw,132px)] font-black tracking-[-0.015em] no-underline [text-shadow:0_0_0_var(--color-tth09-side),0_0_0_var(--color-tth09-side),0_0_0_var(--color-tth09-side),0_0_0_var(--color-tth09-side),0_0_0_var(--color-tth09-side),0_0_0_var(--color-tth09-side),0_0_0_transparent] transition-[translate,text-shadow] duration-300 ease-[cubic-bezier(.34,1.8,.4,1)] hover:translate-x-[0.05em] hover:-translate-y-[0.14em] hover:[text-shadow:.02em_.02em_0_var(--color-tth09-side),.045em_.045em_0_var(--color-tth09-side),.07em_.07em_0_var(--color-tth09-side),.095em_.095em_0_var(--color-tth09-side),.12em_.12em_0_var(--color-tth09-side),.145em_.145em_0_var(--color-tth09-side),.3em_.42em_1em_oklch(0.3_0.08_300/.35)] focus-visible:translate-x-[0.05em] focus-visible:-translate-y-[0.14em] focus-visible:[text-shadow:.02em_.02em_0_var(--color-tth09-side),.045em_.045em_0_var(--color-tth09-side),.07em_.07em_0_var(--color-tth09-side),.095em_.095em_0_var(--color-tth09-side),.12em_.12em_0_var(--color-tth09-side),.145em_.145em_0_var(--color-tth09-side),.3em_.42em_1em_oklch(0.3_0.08_300/.35)] focus-visible:outline-3 focus-visible:outline-offset-[10px] focus-visible:outline-tth09-side active:translate-x-[0.01em] active:translate-y-[0.02em] active:duration-90 active:[text-shadow:.008em_.008em_0_var(--color-tth09-side),.016em_.016em_0_var(--color-tth09-side),.024em_.024em_0_var(--color-tth09-side),.03em_.03em_0_var(--color-tth09-side),.036em_.036em_0_var(--color-tth09-side),.042em_.042em_0_var(--color-tth09-side),.05em_.08em_.2em_oklch(0.3_0.08_300/.3)] motion-reduce:transition-none">JELLY<br>JAM &rsquo;26</a></h2>
    <p class="max-w-[44ch] text-[15px] font-medium leading-[1.65] text-tth09-mut">Two days of wobble-pop, jam stalls and the inflatable orchestra. Hover the headline &mdash; then actually press it.</p>
    <div class="mt-1 flex flex-wrap justify-center gap-3.5">
      <a href="#tth09-top" class="grid min-h-[52px] place-items-center rounded-[14px] border-[2.5px] border-tth09-ink bg-tth09-side px-[26px] text-[15.5px] font-extrabold text-tth09-paper no-underline shadow-[0_0_0_0_var(--color-tth09-ink),0_0_0_0_transparent] transition-[translate,box-shadow] duration-220 ease-[cubic-bezier(.34,1.8,.4,1)] hover:-translate-x-0.5 hover:-translate-y-[5px] hover:shadow-[4px_6px_0_0_var(--color-tth09-ink),10px_16px_26px_-6px_oklch(0.3_0.08_300/0.3)] focus-visible:-translate-y-[5px] focus-visible:shadow-[4px_6px_0_0_var(--color-tth09-ink)] focus-visible:outline-3 focus-visible:outline-offset-[3px] focus-visible:outline-tth09-side active:translate-x-px active:translate-y-0.5 active:shadow-[0_0_0_0_var(--color-tth09-ink),0_0_0_0_transparent] active:duration-80 motion-reduce:transition-none">Get tickets</a>
      <a href="#tth09-top" class="grid min-h-[52px] place-items-center rounded-[14px] border-[2.5px] border-tth09-ink bg-tth09-paper px-[26px] text-[15.5px] font-extrabold no-underline shadow-[0_0_0_0_var(--color-tth09-ink),0_0_0_0_transparent] transition-[translate,box-shadow] duration-220 ease-[cubic-bezier(.34,1.8,.4,1)] hover:-translate-x-0.5 hover:-translate-y-[5px] hover:shadow-[4px_6px_0_0_var(--color-tth09-ink),10px_16px_26px_-6px_oklch(0.3_0.08_300/0.3)] focus-visible:-translate-y-[5px] focus-visible:shadow-[4px_6px_0_0_var(--color-tth09-ink)] focus-visible:outline-3 focus-visible:outline-offset-[3px] focus-visible:outline-tth09-side active:translate-x-px active:translate-y-0.5 active:shadow-[0_0_0_0_var(--color-tth09-ink),0_0_0_0_transparent] active:duration-80 motion-reduce:transition-none">Line-up</a>
    </div>
    <p class="text-[12.5px] text-tth09-mut">Same-length <code class="rounded bg-tth09-chip px-1.5 py-px font-tth09-mono text-[0.9em]">text-shadow</code> lists interpolate &mdash; the extrusion grows, never blinks. <code class="rounded bg-tth09-chip px-1.5 py-px font-tth09-mono text-[0.9em]">active:</code> presses it back into the page.</p>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tth09-bg: #f6efd9;
  --color-tth09-bg: oklch(0.95 0.035 95);
  --color-tth09-glow: #fbf6e6;
  --color-tth09-glow: oklch(0.975 0.025 95);
  --color-tth09-paper: #fbf9f1;
  --color-tth09-paper: oklch(0.98 0.01 95);
  --color-tth09-chip: #eae1c6;
  --color-tth09-chip: oklch(0.9 0.035 95);
  --color-tth09-ink: #3d1c55;
  --color-tth09-ink: oklch(0.3 0.09 305);
  --color-tth09-mut: #5f4c69;
  --color-tth09-mut: oklch(0.45 0.05 300);
  --color-tth09-side: #ee6545;
  --color-tth09-side: oklch(0.68 0.19 30);
  --font-tth09-sans: 'Rubik', system-ui, sans-serif;
  --font-tth09-mono: 'JetBrains Mono', ui-monospace, monospace;
}

.tth-09 {
  width: 100%;
  min-height: 100svh;
}

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-tth09-bg: #f6efd9;
  --color-tth09-bg: oklch(0.95 0.035 95);
  --color-tth09-glow: #fbf6e6;
  --color-tth09-glow: oklch(0.975 0.025 95);
  --color-tth09-paper: #fbf9f1;
  --color-tth09-paper: oklch(0.98 0.01 95);
  --color-tth09-chip: #eae1c6;
  --color-tth09-chip: oklch(0.9 0.035 95);
  --color-tth09-ink: #3d1c55;
  --color-tth09-ink: oklch(0.3 0.09 305);
  --color-tth09-mut: #5f4c69;
  --color-tth09-mut: oklch(0.45 0.05 300);
  --color-tth09-side: #ee6545;
  --color-tth09-side: oklch(0.68 0.19 30);
  --font-tth09-sans: 'Rubik', system-ui, sans-serif;
  --font-tth09-mono: 'JetBrains Mono', ui-monospace, monospace;
}

.tth-09 {
  width: 100%;
  min-height: 100svh;
}
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=Rubik:wght@500..900&family=JetBrains+Mono:wght@400;600&display=swap">

What changed in the port

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

  • Rest, hover and :active shadow lists became three arbitrary [text-shadow:…] values on the link; the 90ms press is active:duration-90 — a state-scoped duration with no extra rule.
  • UI upgrade from the CSS original: Rubik Black's rounded corners make the extrusion read like a moulded jelly sweet instead of a hard Segoe block, and the buttons gained a coloured lift shadow that matches the headline.
  • UI upgrade: muted copy darkened to 0.45 L (6.6:1 on the cream stage; was 5.1:1).
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 3D Text Pop and Lift Hover Effect
Source: https://codefronts.com/motion/tailwind-css-text-hover-effects/tailwind-3d-text-pop-and-lift-hover-effect/

A festival headline that pops out of the page: six same-length text-shadow layers interpolate from 0 to an extruded side, with active: pressing it back in on click. Matching neo-brutalist buttons lift and press with box-shadow. Everything rides a springy cubic-bezier(.34,1.8,.4,1).
## HTML
```html
<section class="tth-09 w-full min-h-svh grid place-items-center bg-tth09-bg bg-[radial-gradient(90%_70%_at_50%_20%,var(--color-tth09-glow),var(--color-tth09-bg)_75%)] px-5 py-14 font-tth09-sans text-tth09-ink" aria-label="3D pop lift text demo">
  <div class="flex flex-col items-center gap-[clamp(16px,3vw,24px)] text-center">
    <p class="text-[12.5px] font-extrabold uppercase tracking-[0.24em] text-tth09-mut">Riverside park &middot; Jan 30&ndash;31 &middot; all ages</p>
    <h2 class="leading-[0.96]"><a id="tth09-top" href="#tth09-top" class="inline-block rounded-md text-[clamp(58px,13vw,132px)] font-black tracking-[-0.015em] no-underline [text-shadow:0_0_0_var(--color-tth09-side),0_0_0_var(--color-tth09-side),0_0_0_var(--color-tth09-side),0_0_0_var(--color-tth09-side),0_0_0_var(--color-tth09-side),0_0_0_var(--color-tth09-side),0_0_0_transparent] transition-[translate,text-shadow] duration-300 ease-[cubic-bezier(.34,1.8,.4,1)] hover:translate-x-[0.05em] hover:-translate-y-[0.14em] hover:[text-shadow:.02em_.02em_0_var(--color-tth09-side),.045em_.045em_0_var(--color-tth09-side),.07em_.07em_0_var(--color-tth09-side),.095em_.095em_0_var(--color-tth09-side),.12em_.12em_0_var(--color-tth09-side),.145em_.145em_0_var(--color-tth09-side),.3em_.42em_1em_oklch(0.3_0.08_300/.35)] focus-visible:translate-x-[0.05em] focus-visible:-translate-y-[0.14em] focus-visible:[text-shadow:.02em_.02em_0_var(--color-tth09-side),.045em_.045em_0_var(--color-tth09-side),.07em_.07em_0_var(--color-tth09-side),.095em_.095em_0_var(--color-tth09-side),.12em_.12em_0_var(--color-tth09-side),.145em_.145em_0_var(--color-tth09-side),.3em_.42em_1em_oklch(0.3_0.08_300/.35)] focus-visible:outline-3 focus-visible:outline-offset-[10px] focus-visible:outline-tth09-side active:translate-x-[0.01em] active:translate-y-[0.02em] active:duration-90 active:[text-shadow:.008em_.008em_0_var(--color-tth09-side),.016em_.016em_0_var(--color-tth09-side),.024em_.024em_0_var(--color-tth09-side),.03em_.03em_0_var(--color-tth09-side),.036em_.036em_0_var(--color-tth09-side),.042em_.042em_0_var(--color-tth09-side),.05em_.08em_.2em_oklch(0.3_0.08_300/.3)] motion-reduce:transition-none">JELLY<br>JAM &rsquo;26</a></h2>
    <p class="max-w-[44ch] text-[15px] font-medium leading-[1.65] text-tth09-mut">Two days of wobble-pop, jam stalls and the inflatable orchestra. Hover the headline &mdash; then actually press it.</p>
    <div class="mt-1 flex flex-wrap justify-center gap-3.5">
      <a href="#tth09-top" class="grid min-h-[52px] place-items-center rounded-[14px] border-[2.5px] border-tth09-ink bg-tth09-side px-[26px] text-[15.5px] font-extrabold text-tth09-paper no-underline shadow-[0_0_0_0_var(--color-tth09-ink),0_0_0_0_transparent] transition-[translate,box-shadow] duration-220 ease-[cubic-bezier(.34,1.8,.4,1)] hover:-translate-x-0.5 hover:-translate-y-[5px] hover:shadow-[4px_6px_0_0_var(--color-tth09-ink),10px_16px_26px_-6px_oklch(0.3_0.08_300/0.3)] focus-visible:-translate-y-[5px] focus-visible:shadow-[4px_6px_0_0_var(--color-tth09-ink)] focus-visible:outline-3 focus-visible:outline-offset-[3px] focus-visible:outline-tth09-side active:translate-x-px active:translate-y-0.5 active:shadow-[0_0_0_0_var(--color-tth09-ink),0_0_0_0_transparent] active:duration-80 motion-reduce:transition-none">Get tickets</a>
      <a href="#tth09-top" class="grid min-h-[52px] place-items-center rounded-[14px] border-[2.5px] border-tth09-ink bg-tth09-paper px-[26px] text-[15.5px] font-extrabold no-underline shadow-[0_0_0_0_var(--color-tth09-ink),0_0_0_0_transparent] transition-[translate,box-shadow] duration-220 ease-[cubic-bezier(.34,1.8,.4,1)] hover:-translate-x-0.5 hover:-translate-y-[5px] hover:shadow-[4px_6px_0_0_var(--color-tth09-ink),10px_16px_26px_-6px_oklch(0.3_0.08_300/0.3)] focus-visible:-translate-y-[5px] focus-visible:shadow-[4px_6px_0_0_var(--color-tth09-ink)] focus-visible:outline-3 focus-visible:outline-offset-[3px] focus-visible:outline-tth09-side active:translate-x-px active:translate-y-0.5 active:shadow-[0_0_0_0_var(--color-tth09-ink),0_0_0_0_transparent] active:duration-80 motion-reduce:transition-none">Line-up</a>
    </div>
    <p class="text-[12.5px] text-tth09-mut">Same-length <code class="rounded bg-tth09-chip px-1.5 py-px font-tth09-mono text-[0.9em]">text-shadow</code> lists interpolate &mdash; the extrusion grows, never blinks. <code class="rounded bg-tth09-chip px-1.5 py-px font-tth09-mono text-[0.9em]">active:</code> presses it back into the page.</p>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tth09-bg: #f6efd9;
  --color-tth09-bg: oklch(0.95 0.035 95);
  --color-tth09-glow: #fbf6e6;
  --color-tth09-glow: oklch(0.975 0.025 95);
  --color-tth09-paper: #fbf9f1;
  --color-tth09-paper: oklch(0.98 0.01 95);
  --color-tth09-chip: #eae1c6;
  --color-tth09-chip: oklch(0.9 0.035 95);
  --color-tth09-ink: #3d1c55;
  --color-tth09-ink: oklch(0.3 0.09 305);
  --color-tth09-mut: #5f4c69;
  --color-tth09-mut: oklch(0.45 0.05 300);
  --color-tth09-side: #ee6545;
  --color-tth09-side: oklch(0.68 0.19 30);
  --font-tth09-sans: 'Rubik', system-ui, sans-serif;
  --font-tth09-mono: 'JetBrains Mono', ui-monospace, monospace;
}

.tth-09 {
  width: 100%;
  min-height: 100svh;
}
```

How this works

Same length, or it blinks. Shadow lists only interpolate when both states have the same number of layers. So the rest state declares six collapsed layers 0 0 0 var(--color-tth09-side) plus a transparent seventh, and hover spreads them along a diagonal:

hover:[text-shadow:.02em_.02em_0_var(--color-tth09-side),
  .045em_.045em_0_…, … ,.145em_.145em_0_…,
  .3em_.42em_1em_oklch(0.3_0.08_300/.35)]

Press. active: shrinks the extrusion to a sliver and shortens the duration to 90ms, so the press snaps while the release springs back over 300ms.

Make it yours

  • Deeper extrusion: add layers (and matching zero layers at rest) in steps of .025em.
  • Colour: --color-tth09-side sets the extruded face; try the ink colour for a monochrome block.
  • Direction: negate the x offsets to extrude down-left.

Gotchas — read before shipping

  • Mismatched layer counts make the shadow jump instead of grow.
  • em offsets keep the extrusion proportional at every clamp() size.
  • Long text-shadow lists are hard to read in markup — move them to --text-shadow-* tokens if you reuse them.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Universal; the floor is Tailwind v4's sheet.

Techniques used in this demo

Utilitiesfirst-class utility or variant

Search CodeFronts

Loading…