22 / 36
All 36 in 36 Tailwind CSS Button Hover Effects
- 01Tailwind Magnetic Ripple Attraction Button Hover Effect
- 02Tailwind Cyberpunk Holographic Glitch Grid Button
- 03Tailwind Frosted Glassmorphism Shimmer Glow Button
- 04Tailwind Organic Blob Morph Button Hover Effect
- 05Tailwind Dual-Line Intersection Border Draw Button
- 06Tailwind Isometric Tactile 3D Depress Button
- 07Tailwind Infinite Sliding Kinetic Marquee Button
- 08Tailwind Chroma Conic Gradient Border Spin Button
- 09Tailwind Minimalist Border & Outline Drawing Hover Effects
- 10Tailwind Modern 3D Skeuomorphic Button Press Effects
- 11Tailwind Neo-Brutalist & Cyberpunk Button Glow Effects
- 12Tailwind Sliding Background Color Wipe Button Effects
- 13Tailwind Liquid Fill & Organic Morphing Button Effects
- 14Tailwind Kinetic Text Flip & Icon Reveal Button Effects
- 15Tailwind Liquid Wave Fill Button Hover Effect
- 16Tailwind Magnetic Border Drift Button
- 17Tailwind Glitch Slice Button Hover Effect
- 18Tailwind Neon Pulse Glow Button Hover Effect
- 19Tailwind Split Reveal Curtain Button Effect
- 20Tailwind Diagonal Shutter Sweep Button Effect
- 21Tailwind Elastic Lift Spring Button Hover Effect
- 22Tailwind Center Underline Draw Link Button Effects
- 23Tailwind 3D Flip Card Button Hover Effect
- 24Tailwind Particle Burst Confetti Button Effect
- 25Tailwind Slide Doors Closing Panel Button Effect
- 26Tailwind Typewriter Retype Terminal Button Effect
- 27Tailwind Hard Shadow Shift Flat Button Effect
- 28Tailwind Material Ripple Wave Button Effect
- 29Tailwind Rainbow Gradient Border Spin Button
- 30Tailwind Morph Shape Silhouette Button Effects
- 31Tailwind Icon Slide In Directional Button Effect
- 32Tailwind Cursor Spotlight Glow Button
- 33Tailwind Stagger Fill Equaliser Button Effect
- 34Tailwind Sunburst Expand Radiant Button Effect
- 35Tailwind 3D Press Plunger Key Button Effect
- 36Tailwind Add-to-Cart Liquid Glass Ripple Button
Tailwind Center Underline Draw Link Button Effects
The minimal one: a crisp underline grows outward from the centre to full width on hover — the cleanest affordance for nav links and inline CTAs, in three treatments. Design B contains the trick worth stealing: swapping transform-origin between states turns a grow into a directional swipe.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<div class="tbh-22-group w-full min-h-svh flex flex-wrap">
<section class="tbh-22a flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh22-sans bg-tbh22a-stage" aria-label="Center underline button">
<button type="button" class="relative cursor-pointer border-0 bg-transparent px-1 pt-1.5 pb-2.5 text-lg font-bold text-tbh22a-ink transition-colors duration-200 hover:text-tbh22a-line focus-visible:text-tbh22a-line focus-visible:outline-none after:content-[''] after:absolute after:left-0 after:bottom-0 after:w-full after:h-0.5 after:bg-tbh22a-line after:scale-x-0 after:origin-center after:transition-[scale,height] after:duration-300 after:ease-out hover:after:scale-x-100 focus-visible:after:scale-x-100 focus-visible:after:h-[3px] motion-reduce:after:transition-none">
Learn more
</button>
</section>
<section class="tbh-22b flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh22-serif bg-tbh22b-stage" aria-label="Ink swipe underline button">
<button type="button" class="relative cursor-pointer border-0 bg-transparent px-0.5 pt-1 pb-2.5 text-xl italic text-tbh22b-ink transition-colors duration-200 hover:text-tbh22b-line focus-visible:text-tbh22b-line focus-visible:outline-none after:content-[''] after:absolute after:left-0 after:bottom-0.5 after:w-full after:h-[3px] after:bg-tbh22b-line after:scale-x-0 after:origin-right after:transition-[scale] after:duration-[350ms] after:ease-[cubic-bezier(.6,0,.2,1)] hover:after:scale-x-100 hover:after:origin-left focus-visible:after:scale-x-100 focus-visible:after:origin-left motion-reduce:after:transition-none">
Read essay
</button>
</section>
<section class="tbh-22c flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh22-sans bg-tbh22c-stage" aria-label="Double rule underline button">
<button type="button" class="relative cursor-pointer border-0 bg-transparent px-1 pt-1.5 pb-3.5 text-lg font-bold text-tbh22c-ink transition-colors duration-200 hover:text-tbh22c-line focus-visible:text-tbh22c-line focus-visible:outline-none before:content-[''] before:absolute before:left-0 before:bottom-1.5 before:w-full before:h-0.5 before:bg-tbh22c-line before:scale-x-0 before:origin-left before:transition-[scale] before:duration-300 before:ease-out after:content-[''] after:absolute after:left-0 after:bottom-0 after:w-full after:h-0.5 after:bg-tbh22c-line after:scale-x-0 after:origin-right after:transition-[scale] after:duration-300 after:delay-[80ms] after:ease-out hover:before:scale-x-100 hover:after:scale-x-100 focus-visible:before:scale-x-100 focus-visible:after:scale-x-100 motion-reduce:before:transition-none motion-reduce:after:transition-none motion-reduce:after:delay-0">
See pricing
</button>
</section>
</div><div class="tbh-22-group w-full min-h-svh flex flex-wrap">
<section class="tbh-22a flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh22-sans bg-tbh22a-stage" aria-label="Center underline button">
<button type="button" class="relative cursor-pointer border-0 bg-transparent px-1 pt-1.5 pb-2.5 text-lg font-bold text-tbh22a-ink transition-colors duration-200 hover:text-tbh22a-line focus-visible:text-tbh22a-line focus-visible:outline-none after:content-[''] after:absolute after:left-0 after:bottom-0 after:w-full after:h-0.5 after:bg-tbh22a-line after:scale-x-0 after:origin-center after:transition-[scale,height] after:duration-300 after:ease-out hover:after:scale-x-100 focus-visible:after:scale-x-100 focus-visible:after:h-[3px] motion-reduce:after:transition-none">
Learn more
</button>
</section>
<section class="tbh-22b flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh22-serif bg-tbh22b-stage" aria-label="Ink swipe underline button">
<button type="button" class="relative cursor-pointer border-0 bg-transparent px-0.5 pt-1 pb-2.5 text-xl italic text-tbh22b-ink transition-colors duration-200 hover:text-tbh22b-line focus-visible:text-tbh22b-line focus-visible:outline-none after:content-[''] after:absolute after:left-0 after:bottom-0.5 after:w-full after:h-[3px] after:bg-tbh22b-line after:scale-x-0 after:origin-right after:transition-[scale] after:duration-[350ms] after:ease-[cubic-bezier(.6,0,.2,1)] hover:after:scale-x-100 hover:after:origin-left focus-visible:after:scale-x-100 focus-visible:after:origin-left motion-reduce:after:transition-none">
Read essay
</button>
</section>
<section class="tbh-22c flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh22-sans bg-tbh22c-stage" aria-label="Double rule underline button">
<button type="button" class="relative cursor-pointer border-0 bg-transparent px-1 pt-1.5 pb-3.5 text-lg font-bold text-tbh22c-ink transition-colors duration-200 hover:text-tbh22c-line focus-visible:text-tbh22c-line focus-visible:outline-none before:content-[''] before:absolute before:left-0 before:bottom-1.5 before:w-full before:h-0.5 before:bg-tbh22c-line before:scale-x-0 before:origin-left before:transition-[scale] before:duration-300 before:ease-out after:content-[''] after:absolute after:left-0 after:bottom-0 after:w-full after:h-0.5 after:bg-tbh22c-line after:scale-x-0 after:origin-right after:transition-[scale] after:duration-300 after:delay-[80ms] after:ease-out hover:before:scale-x-100 hover:after:scale-x-100 focus-visible:before:scale-x-100 focus-visible:after:scale-x-100 motion-reduce:before:transition-none motion-reduce:after:transition-none motion-reduce:after:delay-0">
See pricing
</button>
</section>
</div>@import "tailwindcss";
@theme {
--color-tbh22a-line: #3f74d8;
--color-tbh22a-line: oklch(0.55 0.18 255);
--color-tbh22a-ink: #1a1a1a;
--color-tbh22a-ink: oklch(0.22 0 0);
--color-tbh22a-stage: #fafafa;
--color-tbh22a-stage: oklch(0.98 0 0);
--color-tbh22b-line: #a8452c;
--color-tbh22b-line: oklch(0.5 0.15 25);
--color-tbh22b-ink: #241c17;
--color-tbh22b-ink: oklch(0.25 0.02 45);
--color-tbh22b-stage: #fbf8f3;
--color-tbh22b-stage: oklch(0.98 0.008 85);
--color-tbh22c-line: #3d8e96;
--color-tbh22c-line: oklch(0.55 0.14 200);
--color-tbh22c-ink: #0e2a2c;
--color-tbh22c-ink: oklch(0.28 0.03 200);
--color-tbh22c-stage: #f4f8f8;
--color-tbh22c-stage: oklch(0.97 0.008 200);
--font-tbh22-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tbh22-serif: 'Newsreader', Georgia, serif;
}
/* Nothing stays CSS: three underline treatments, all utilities. */
.tbh-22-group {
width: 100%;
min-height: 100svh;
}
.tbh-22a,
.tbh-22b,
.tbh-22c {
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tbh22a-line: #3f74d8;
--color-tbh22a-line: oklch(0.55 0.18 255);
--color-tbh22a-ink: #1a1a1a;
--color-tbh22a-ink: oklch(0.22 0 0);
--color-tbh22a-stage: #fafafa;
--color-tbh22a-stage: oklch(0.98 0 0);
--color-tbh22b-line: #a8452c;
--color-tbh22b-line: oklch(0.5 0.15 25);
--color-tbh22b-ink: #241c17;
--color-tbh22b-ink: oklch(0.25 0.02 45);
--color-tbh22b-stage: #fbf8f3;
--color-tbh22b-stage: oklch(0.98 0.008 85);
--color-tbh22c-line: #3d8e96;
--color-tbh22c-line: oklch(0.55 0.14 200);
--color-tbh22c-ink: #0e2a2c;
--color-tbh22c-ink: oklch(0.28 0.03 200);
--color-tbh22c-stage: #f4f8f8;
--color-tbh22c-stage: oklch(0.97 0.008 200);
--font-tbh22-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tbh22-serif: 'Newsreader', Georgia, serif;
}
/* Nothing stays CSS: three underline treatments, all utilities. */
.tbh-22-group {
width: 100%;
min-height: 100svh;
}
.tbh-22a,
.tbh-22b,
.tbh-22c {
min-height: 100svh;
}Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<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.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-tbh22a-line: #3f74d8;
--color-tbh22a-line: oklch(0.55 0.18 255);
--color-tbh22a-ink: #1a1a1a;
--color-tbh22a-ink: oklch(0.22 0 0);
--color-tbh22a-stage: #fafafa;
--color-tbh22a-stage: oklch(0.98 0 0);
--color-tbh22b-line: #a8452c;
--color-tbh22b-line: oklch(0.5 0.15 25);
--color-tbh22b-ink: #241c17;
--color-tbh22b-ink: oklch(0.25 0.02 45);
--color-tbh22b-stage: #fbf8f3;
--color-tbh22b-stage: oklch(0.98 0.008 85);
--color-tbh22c-line: #3d8e96;
--color-tbh22c-line: oklch(0.55 0.14 200);
--color-tbh22c-ink: #0e2a2c;
--color-tbh22c-ink: oklch(0.28 0.03 200);
--color-tbh22c-stage: #f4f8f8;
--color-tbh22c-stage: oklch(0.97 0.008 200);
--font-tbh22-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tbh22-serif: 'Newsreader', Georgia, serif;
}
/* Nothing stays CSS: three underline treatments, all utilities. */
.tbh-22-group {
width: 100%;
min-height: 100svh;
}
.tbh-22a,
.tbh-22b,
.tbh-22c {
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tbh22a-line: #3f74d8;
--color-tbh22a-line: oklch(0.55 0.18 255);
--color-tbh22a-ink: #1a1a1a;
--color-tbh22a-ink: oklch(0.22 0 0);
--color-tbh22a-stage: #fafafa;
--color-tbh22a-stage: oklch(0.98 0 0);
--color-tbh22b-line: #a8452c;
--color-tbh22b-line: oklch(0.5 0.15 25);
--color-tbh22b-ink: #241c17;
--color-tbh22b-ink: oklch(0.25 0.02 45);
--color-tbh22b-stage: #fbf8f3;
--color-tbh22b-stage: oklch(0.98 0.008 85);
--color-tbh22c-line: #3d8e96;
--color-tbh22c-line: oklch(0.55 0.14 200);
--color-tbh22c-ink: #0e2a2c;
--color-tbh22c-ink: oklch(0.28 0.03 200);
--color-tbh22c-stage: #f4f8f8;
--color-tbh22c-stage: oklch(0.97 0.008 200);
--font-tbh22-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tbh22-serif: 'Newsreader', Georgia, serif;
}
/* Nothing stays CSS: three underline treatments, all utilities. */
.tbh-22-group {
width: 100%;
min-height: 100svh;
}
.tbh-22a,
.tbh-22b,
.tbh-22c {
min-height: 100svh;
}<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=Newsreader:ital,wght@1,400;1,500&display=swap">
<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=Newsreader:ital,wght@1,400;1,500&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Design A's
left:50%; width:0; translateX(-50%)centring collapses toafter:origin-center after:scale-x-0— two utilities replacing three coupled declarations, with no layout animation left in the effect. - The origin swap in design B (
after:origin-right→hover:after:origin-left) is the kind of state change Tailwind expresses better than CSS does: both origins are visible in one class list, so the direction of travel is readable instead of inferred from two rules. - UI change from the CSS original: design A's focus state now thickens the rule to 3px as its indicator (the original left focus and hover identical), and design B's serif is a real webfont rather than a Georgia fallback.
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 Center Underline Draw Link Button Effects
Source: https://codefronts.com/motion/tailwind-css-button-hover-effects/tailwind-center-underline-draw-link-button-effects/
The minimal one: a crisp underline grows outward from the centre to full width on hover — the cleanest affordance for nav links and inline CTAs, in three treatments. Design B contains the trick worth stealing: swapping transform-origin between states turns a grow into a directional swipe.
## HTML
```html
<div class="tbh-22-group w-full min-h-svh flex flex-wrap">
<section class="tbh-22a flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh22-sans bg-tbh22a-stage" aria-label="Center underline button">
<button type="button" class="relative cursor-pointer border-0 bg-transparent px-1 pt-1.5 pb-2.5 text-lg font-bold text-tbh22a-ink transition-colors duration-200 hover:text-tbh22a-line focus-visible:text-tbh22a-line focus-visible:outline-none after:content-[''] after:absolute after:left-0 after:bottom-0 after:w-full after:h-0.5 after:bg-tbh22a-line after:scale-x-0 after:origin-center after:transition-[scale,height] after:duration-300 after:ease-out hover:after:scale-x-100 focus-visible:after:scale-x-100 focus-visible:after:h-[3px] motion-reduce:after:transition-none">
Learn more
</button>
</section>
<section class="tbh-22b flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh22-serif bg-tbh22b-stage" aria-label="Ink swipe underline button">
<button type="button" class="relative cursor-pointer border-0 bg-transparent px-0.5 pt-1 pb-2.5 text-xl italic text-tbh22b-ink transition-colors duration-200 hover:text-tbh22b-line focus-visible:text-tbh22b-line focus-visible:outline-none after:content-[''] after:absolute after:left-0 after:bottom-0.5 after:w-full after:h-[3px] after:bg-tbh22b-line after:scale-x-0 after:origin-right after:transition-[scale] after:duration-[350ms] after:ease-[cubic-bezier(.6,0,.2,1)] hover:after:scale-x-100 hover:after:origin-left focus-visible:after:scale-x-100 focus-visible:after:origin-left motion-reduce:after:transition-none">
Read essay
</button>
</section>
<section class="tbh-22c flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh22-sans bg-tbh22c-stage" aria-label="Double rule underline button">
<button type="button" class="relative cursor-pointer border-0 bg-transparent px-1 pt-1.5 pb-3.5 text-lg font-bold text-tbh22c-ink transition-colors duration-200 hover:text-tbh22c-line focus-visible:text-tbh22c-line focus-visible:outline-none before:content-[''] before:absolute before:left-0 before:bottom-1.5 before:w-full before:h-0.5 before:bg-tbh22c-line before:scale-x-0 before:origin-left before:transition-[scale] before:duration-300 before:ease-out after:content-[''] after:absolute after:left-0 after:bottom-0 after:w-full after:h-0.5 after:bg-tbh22c-line after:scale-x-0 after:origin-right after:transition-[scale] after:duration-300 after:delay-[80ms] after:ease-out hover:before:scale-x-100 hover:after:scale-x-100 focus-visible:before:scale-x-100 focus-visible:after:scale-x-100 motion-reduce:before:transition-none motion-reduce:after:transition-none motion-reduce:after:delay-0">
See pricing
</button>
</section>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tbh22a-line: #3f74d8;
--color-tbh22a-line: oklch(0.55 0.18 255);
--color-tbh22a-ink: #1a1a1a;
--color-tbh22a-ink: oklch(0.22 0 0);
--color-tbh22a-stage: #fafafa;
--color-tbh22a-stage: oklch(0.98 0 0);
--color-tbh22b-line: #a8452c;
--color-tbh22b-line: oklch(0.5 0.15 25);
--color-tbh22b-ink: #241c17;
--color-tbh22b-ink: oklch(0.25 0.02 45);
--color-tbh22b-stage: #fbf8f3;
--color-tbh22b-stage: oklch(0.98 0.008 85);
--color-tbh22c-line: #3d8e96;
--color-tbh22c-line: oklch(0.55 0.14 200);
--color-tbh22c-ink: #0e2a2c;
--color-tbh22c-ink: oklch(0.28 0.03 200);
--color-tbh22c-stage: #f4f8f8;
--color-tbh22c-stage: oklch(0.97 0.008 200);
--font-tbh22-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tbh22-serif: 'Newsreader', Georgia, serif;
}
/* Nothing stays CSS: three underline treatments, all utilities. */
.tbh-22-group {
width: 100%;
min-height: 100svh;
}
.tbh-22a,
.tbh-22b,
.tbh-22c {
min-height: 100svh;
}
```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 Center Underline Draw Link Button Effects
Source: https://codefronts.com/motion/tailwind-css-button-hover-effects/tailwind-center-underline-draw-link-button-effects/
The minimal one: a crisp underline grows outward from the centre to full width on hover — the cleanest affordance for nav links and inline CTAs, in three treatments. Design B contains the trick worth stealing: swapping transform-origin between states turns a grow into a directional swipe.
## HTML
```html
<div class="tbh-22-group w-full min-h-svh flex flex-wrap">
<section class="tbh-22a flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh22-sans bg-tbh22a-stage" aria-label="Center underline button">
<button type="button" class="relative cursor-pointer border-0 bg-transparent px-1 pt-1.5 pb-2.5 text-lg font-bold text-tbh22a-ink transition-colors duration-200 hover:text-tbh22a-line focus-visible:text-tbh22a-line focus-visible:outline-none after:content-[''] after:absolute after:left-0 after:bottom-0 after:w-full after:h-0.5 after:bg-tbh22a-line after:scale-x-0 after:origin-center after:transition-[scale,height] after:duration-300 after:ease-out hover:after:scale-x-100 focus-visible:after:scale-x-100 focus-visible:after:h-[3px] motion-reduce:after:transition-none">
Learn more
</button>
</section>
<section class="tbh-22b flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh22-serif bg-tbh22b-stage" aria-label="Ink swipe underline button">
<button type="button" class="relative cursor-pointer border-0 bg-transparent px-0.5 pt-1 pb-2.5 text-xl italic text-tbh22b-ink transition-colors duration-200 hover:text-tbh22b-line focus-visible:text-tbh22b-line focus-visible:outline-none after:content-[''] after:absolute after:left-0 after:bottom-0.5 after:w-full after:h-[3px] after:bg-tbh22b-line after:scale-x-0 after:origin-right after:transition-[scale] after:duration-[350ms] after:ease-[cubic-bezier(.6,0,.2,1)] hover:after:scale-x-100 hover:after:origin-left focus-visible:after:scale-x-100 focus-visible:after:origin-left motion-reduce:after:transition-none">
Read essay
</button>
</section>
<section class="tbh-22c flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh22-sans bg-tbh22c-stage" aria-label="Double rule underline button">
<button type="button" class="relative cursor-pointer border-0 bg-transparent px-1 pt-1.5 pb-3.5 text-lg font-bold text-tbh22c-ink transition-colors duration-200 hover:text-tbh22c-line focus-visible:text-tbh22c-line focus-visible:outline-none before:content-[''] before:absolute before:left-0 before:bottom-1.5 before:w-full before:h-0.5 before:bg-tbh22c-line before:scale-x-0 before:origin-left before:transition-[scale] before:duration-300 before:ease-out after:content-[''] after:absolute after:left-0 after:bottom-0 after:w-full after:h-0.5 after:bg-tbh22c-line after:scale-x-0 after:origin-right after:transition-[scale] after:duration-300 after:delay-[80ms] after:ease-out hover:before:scale-x-100 hover:after:scale-x-100 focus-visible:before:scale-x-100 focus-visible:after:scale-x-100 motion-reduce:before:transition-none motion-reduce:after:transition-none motion-reduce:after:delay-0">
See pricing
</button>
</section>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tbh22a-line: #3f74d8;
--color-tbh22a-line: oklch(0.55 0.18 255);
--color-tbh22a-ink: #1a1a1a;
--color-tbh22a-ink: oklch(0.22 0 0);
--color-tbh22a-stage: #fafafa;
--color-tbh22a-stage: oklch(0.98 0 0);
--color-tbh22b-line: #a8452c;
--color-tbh22b-line: oklch(0.5 0.15 25);
--color-tbh22b-ink: #241c17;
--color-tbh22b-ink: oklch(0.25 0.02 45);
--color-tbh22b-stage: #fbf8f3;
--color-tbh22b-stage: oklch(0.98 0.008 85);
--color-tbh22c-line: #3d8e96;
--color-tbh22c-line: oklch(0.55 0.14 200);
--color-tbh22c-ink: #0e2a2c;
--color-tbh22c-ink: oklch(0.28 0.03 200);
--color-tbh22c-stage: #f4f8f8;
--color-tbh22c-stage: oklch(0.97 0.008 200);
--font-tbh22-sans: 'Space Grotesk', system-ui, sans-serif;
--font-tbh22-serif: 'Newsreader', Georgia, serif;
}
/* Nothing stays CSS: three underline treatments, all utilities. */
.tbh-22-group {
width: 100%;
min-height: 100svh;
}
.tbh-22a,
.tbh-22b,
.tbh-22c {
min-height: 100svh;
}
```How this works
Scale beats width, again. The CSS original animated width: 0 → 100% with a left:50% translateX(-50%) centring hack. after:scale-x-0 after:origin-center gets the identical symmetric draw with no layout work and no centring arithmetic — and because scale is its own property in v4, the centring is not entangled with it.
Origin is a state, not a constant. Design B's rule sits at full width, scaled to 0 from the right. On hover the origin flips to the left and the scale goes to 1, so the line appears to travel through the text. One utility in each state, and an effect that is genuinely hard to read in plain CSS.
Focus is the underline. These buttons have no box to ring, so dropping the outline is justified only because the underline takes over: at focus-visible it draws AND thickens to 3px. That is a visible, non-motion indicator.
Make it yours
- Weight and colour:
after:h-0.5andafter:bg-tbh22a-line; the hover ink colour reads the same token. - Direction:
after:origin-center(out from the middle),origin-left(left to right), or B's swap for a swipe. - Stagger: design C's
after:delay-[80ms]— enough to read as two strokes, short enough to feel like one gesture. - Pace:
after:duration-300; a serif editorial link can afford 350ms, a dense nav should stay at 200ms.
Gotchas — read before shipping
- Pad the label's bottom (
pb-2.5/pb-3.5) so the rule clears descenders — an underline through a 'g' looks like a strikethrough. - Removing
focus-visible:outlineis only acceptable because the underline draws and thickens on focus. Drop that twin and the buttons become keyboard-invisible. - The resting label must be legible on its own: these are text buttons, so contrast is carried entirely by the ink colour, not by a fill.
- Design C's two rules need different
bottomoffsets (bottom-1.5andbottom-0); equal offsets stack them into one 4px line.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Pseudo-elements, scale transforms and transition-delay are universal; with CSS unavailable all three render as plain text labels.