12 / 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 Sliding Background Color Wipe Button Effects
The dependable one: a solid colour wipes in and the label colour swaps to stay readable — the most requested hover in professional product UIs, in three directions. The port's whole subtlety is one utility: delay-[120ms] on the label's colour transition.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<div class="tbh-12-group w-full min-h-svh flex flex-wrap">
<section class="tbh-12a flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh12-sans bg-tbh12a-stage" aria-label="Colour wipe button">
<button type="button" class="relative isolate overflow-hidden cursor-pointer rounded-[10px] border-2 border-tbh12a-fill px-[38px] py-4 text-base font-bold text-tbh12a-fill bg-transparent transition-colors duration-[250ms] delay-[120ms] before:content-[''] before:absolute before:inset-0 before:z-0 before:bg-tbh12a-fill before:scale-x-0 before:origin-left before:transition-transform before:duration-400 before:ease-[cubic-bezier(.4,0,.2,1)] hover:text-tbh12a-onfill hover:before:scale-x-100 focus-visible:text-tbh12a-onfill focus-visible:before:scale-x-100 focus-visible:outline-3 focus-visible:outline-tbh12a-fill focus-visible:outline-offset-[3px] motion-reduce:transition-none motion-reduce:before:transition-none">
<span class="relative z-[1]">Contact sales</span>
</button>
</section>
<section class="tbh-12b flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh12-sans bg-tbh12b-stage" aria-label="Bottom-up fill button">
<button type="button" class="relative isolate overflow-hidden cursor-pointer rounded-[10px] border-2 border-tbh12b-fill px-[42px] py-[17px] text-base font-extrabold text-tbh12b-fill bg-transparent transition-colors duration-[250ms] delay-[140ms] before:content-[''] before:absolute before:inset-0 before:z-0 before:bg-tbh12b-fill before:scale-y-0 before:origin-bottom before:transition-transform before:duration-[380ms] before:ease-[cubic-bezier(.4,0,.2,1)] hover:text-tbh12b-onfill hover:before:scale-y-100 focus-visible:text-tbh12b-onfill focus-visible:before:scale-y-100 focus-visible:outline-3 focus-visible:outline-tbh12b-fill focus-visible:outline-offset-[3px] motion-reduce:transition-none motion-reduce:before:transition-none">
<span class="relative z-[1]">Get the app</span>
</button>
</section>
<section class="tbh-12c flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh12-sans bg-tbh12c-stage" aria-label="Center-out fill button">
<button type="button" class="relative isolate overflow-hidden cursor-pointer rounded-full border-2 border-tbh12c-fill px-[42px] py-[17px] text-base font-extrabold text-tbh12c-fill bg-transparent transition-colors duration-[250ms] delay-[120ms] before:content-[''] before:absolute before:inset-0 before:z-0 before:bg-tbh12c-fill before:scale-x-0 before:origin-center before:transition-transform before:duration-400 before:ease-[cubic-bezier(.4,0,.2,1)] hover:text-tbh12c-onfill hover:before:scale-x-100 focus-visible:text-tbh12c-onfill focus-visible:before:scale-x-100 focus-visible:outline-3 focus-visible:outline-tbh12c-fill focus-visible:outline-offset-[3px] motion-reduce:transition-none motion-reduce:before:transition-none">
<span class="relative z-[1]">Start trial</span>
</button>
</section>
</div><div class="tbh-12-group w-full min-h-svh flex flex-wrap">
<section class="tbh-12a flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh12-sans bg-tbh12a-stage" aria-label="Colour wipe button">
<button type="button" class="relative isolate overflow-hidden cursor-pointer rounded-[10px] border-2 border-tbh12a-fill px-[38px] py-4 text-base font-bold text-tbh12a-fill bg-transparent transition-colors duration-[250ms] delay-[120ms] before:content-[''] before:absolute before:inset-0 before:z-0 before:bg-tbh12a-fill before:scale-x-0 before:origin-left before:transition-transform before:duration-400 before:ease-[cubic-bezier(.4,0,.2,1)] hover:text-tbh12a-onfill hover:before:scale-x-100 focus-visible:text-tbh12a-onfill focus-visible:before:scale-x-100 focus-visible:outline-3 focus-visible:outline-tbh12a-fill focus-visible:outline-offset-[3px] motion-reduce:transition-none motion-reduce:before:transition-none">
<span class="relative z-[1]">Contact sales</span>
</button>
</section>
<section class="tbh-12b flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh12-sans bg-tbh12b-stage" aria-label="Bottom-up fill button">
<button type="button" class="relative isolate overflow-hidden cursor-pointer rounded-[10px] border-2 border-tbh12b-fill px-[42px] py-[17px] text-base font-extrabold text-tbh12b-fill bg-transparent transition-colors duration-[250ms] delay-[140ms] before:content-[''] before:absolute before:inset-0 before:z-0 before:bg-tbh12b-fill before:scale-y-0 before:origin-bottom before:transition-transform before:duration-[380ms] before:ease-[cubic-bezier(.4,0,.2,1)] hover:text-tbh12b-onfill hover:before:scale-y-100 focus-visible:text-tbh12b-onfill focus-visible:before:scale-y-100 focus-visible:outline-3 focus-visible:outline-tbh12b-fill focus-visible:outline-offset-[3px] motion-reduce:transition-none motion-reduce:before:transition-none">
<span class="relative z-[1]">Get the app</span>
</button>
</section>
<section class="tbh-12c flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh12-sans bg-tbh12c-stage" aria-label="Center-out fill button">
<button type="button" class="relative isolate overflow-hidden cursor-pointer rounded-full border-2 border-tbh12c-fill px-[42px] py-[17px] text-base font-extrabold text-tbh12c-fill bg-transparent transition-colors duration-[250ms] delay-[120ms] before:content-[''] before:absolute before:inset-0 before:z-0 before:bg-tbh12c-fill before:scale-x-0 before:origin-center before:transition-transform before:duration-400 before:ease-[cubic-bezier(.4,0,.2,1)] hover:text-tbh12c-onfill hover:before:scale-x-100 focus-visible:text-tbh12c-onfill focus-visible:before:scale-x-100 focus-visible:outline-3 focus-visible:outline-tbh12c-fill focus-visible:outline-offset-[3px] motion-reduce:transition-none motion-reduce:before:transition-none">
<span class="relative z-[1]">Start trial</span>
</button>
</section>
</div>@import "tailwindcss";
@theme {
--color-tbh12a-fill: #3f6ad0;
--color-tbh12a-fill: oklch(0.55 0.17 255);
--color-tbh12a-onfill: #ffffff;
--color-tbh12a-onfill: oklch(1 0 0);
--color-tbh12a-stage: #eef1f6;
--color-tbh12a-stage: oklch(0.95 0.008 255);
--color-tbh12b-fill: #4cb87a;
--color-tbh12b-fill: oklch(0.6 0.16 150);
--color-tbh12b-onfill: #04180d;
--color-tbh12b-onfill: oklch(0.2 0.05 150);
--color-tbh12b-stage: #0d160f;
--color-tbh12b-stage: oklch(0.16 0.02 150);
--color-tbh12c-fill: #d2543c;
--color-tbh12c-fill: oklch(0.58 0.18 20);
--color-tbh12c-onfill: #ffffff;
--color-tbh12c-onfill: oklch(1 0 0);
--color-tbh12c-stage: #1a0f10;
--color-tbh12c-stage: oklch(0.18 0.02 20);
--font-tbh12-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* Nothing stays CSS. Three directions, three origin utilities, one delayed colour swap. */
.tbh-12-group {
width: 100%;
min-height: 100svh;
}
.tbh-12a,
.tbh-12b,
.tbh-12c {
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tbh12a-fill: #3f6ad0;
--color-tbh12a-fill: oklch(0.55 0.17 255);
--color-tbh12a-onfill: #ffffff;
--color-tbh12a-onfill: oklch(1 0 0);
--color-tbh12a-stage: #eef1f6;
--color-tbh12a-stage: oklch(0.95 0.008 255);
--color-tbh12b-fill: #4cb87a;
--color-tbh12b-fill: oklch(0.6 0.16 150);
--color-tbh12b-onfill: #04180d;
--color-tbh12b-onfill: oklch(0.2 0.05 150);
--color-tbh12b-stage: #0d160f;
--color-tbh12b-stage: oklch(0.16 0.02 150);
--color-tbh12c-fill: #d2543c;
--color-tbh12c-fill: oklch(0.58 0.18 20);
--color-tbh12c-onfill: #ffffff;
--color-tbh12c-onfill: oklch(1 0 0);
--color-tbh12c-stage: #1a0f10;
--color-tbh12c-stage: oklch(0.18 0.02 20);
--font-tbh12-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* Nothing stays CSS. Three directions, three origin utilities, one delayed colour swap. */
.tbh-12-group {
width: 100%;
min-height: 100svh;
}
.tbh-12a,
.tbh-12b,
.tbh-12c {
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-tbh12a-fill: #3f6ad0;
--color-tbh12a-fill: oklch(0.55 0.17 255);
--color-tbh12a-onfill: #ffffff;
--color-tbh12a-onfill: oklch(1 0 0);
--color-tbh12a-stage: #eef1f6;
--color-tbh12a-stage: oklch(0.95 0.008 255);
--color-tbh12b-fill: #4cb87a;
--color-tbh12b-fill: oklch(0.6 0.16 150);
--color-tbh12b-onfill: #04180d;
--color-tbh12b-onfill: oklch(0.2 0.05 150);
--color-tbh12b-stage: #0d160f;
--color-tbh12b-stage: oklch(0.16 0.02 150);
--color-tbh12c-fill: #d2543c;
--color-tbh12c-fill: oklch(0.58 0.18 20);
--color-tbh12c-onfill: #ffffff;
--color-tbh12c-onfill: oklch(1 0 0);
--color-tbh12c-stage: #1a0f10;
--color-tbh12c-stage: oklch(0.18 0.02 20);
--font-tbh12-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* Nothing stays CSS. Three directions, three origin utilities, one delayed colour swap. */
.tbh-12-group {
width: 100%;
min-height: 100svh;
}
.tbh-12a,
.tbh-12b,
.tbh-12c {
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tbh12a-fill: #3f6ad0;
--color-tbh12a-fill: oklch(0.55 0.17 255);
--color-tbh12a-onfill: #ffffff;
--color-tbh12a-onfill: oklch(1 0 0);
--color-tbh12a-stage: #eef1f6;
--color-tbh12a-stage: oklch(0.95 0.008 255);
--color-tbh12b-fill: #4cb87a;
--color-tbh12b-fill: oklch(0.6 0.16 150);
--color-tbh12b-onfill: #04180d;
--color-tbh12b-onfill: oklch(0.2 0.05 150);
--color-tbh12b-stage: #0d160f;
--color-tbh12b-stage: oklch(0.16 0.02 150);
--color-tbh12c-fill: #d2543c;
--color-tbh12c-fill: oklch(0.58 0.18 20);
--color-tbh12c-onfill: #ffffff;
--color-tbh12c-onfill: oklch(1 0 0);
--color-tbh12c-stage: #1a0f10;
--color-tbh12c-stage: oklch(0.18 0.02 20);
--font-tbh12-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* Nothing stays CSS. Three directions, three origin utilities, one delayed colour swap. */
.tbh-12-group {
width: 100%;
min-height: 100svh;
}
.tbh-12a,
.tbh-12b,
.tbh-12c {
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&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&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- The batch's most ordinary effect and its most instructive port: the direction of the wipe is now literally one word in the class list (
before:origin-left/origin-bottom/origin-center), so the three variants are diffable at a glance instead of three near-identical rule blocks. delay-[120ms]ontransition-colorsis the whole reason this pattern looks professional, and putting it in the class list is the first time it is visible to whoever copies the markup. In the CSS version it was the second number in a shorthand.- UI change from the CSS original: the fill and the ink swap now both fire on
focus-visible:, and the-onfillcolours were promoted to tokens so the on-fill contrast is stated rather than assumed — design B's dark ink on green is the one that needed it.
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 Sliding Background Color Wipe Button Effects
Source: https://codefronts.com/motion/tailwind-css-button-hover-effects/tailwind-sliding-background-color-wipe-button-effects/
The dependable one: a solid colour wipes in and the label colour swaps to stay readable — the most requested hover in professional product UIs, in three directions. The port's whole subtlety is one utility: delay-[120ms] on the label's colour transition.
## HTML
```html
<div class="tbh-12-group w-full min-h-svh flex flex-wrap">
<section class="tbh-12a flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh12-sans bg-tbh12a-stage" aria-label="Colour wipe button">
<button type="button" class="relative isolate overflow-hidden cursor-pointer rounded-[10px] border-2 border-tbh12a-fill px-[38px] py-4 text-base font-bold text-tbh12a-fill bg-transparent transition-colors duration-[250ms] delay-[120ms] before:content-[''] before:absolute before:inset-0 before:z-0 before:bg-tbh12a-fill before:scale-x-0 before:origin-left before:transition-transform before:duration-400 before:ease-[cubic-bezier(.4,0,.2,1)] hover:text-tbh12a-onfill hover:before:scale-x-100 focus-visible:text-tbh12a-onfill focus-visible:before:scale-x-100 focus-visible:outline-3 focus-visible:outline-tbh12a-fill focus-visible:outline-offset-[3px] motion-reduce:transition-none motion-reduce:before:transition-none">
<span class="relative z-[1]">Contact sales</span>
</button>
</section>
<section class="tbh-12b flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh12-sans bg-tbh12b-stage" aria-label="Bottom-up fill button">
<button type="button" class="relative isolate overflow-hidden cursor-pointer rounded-[10px] border-2 border-tbh12b-fill px-[42px] py-[17px] text-base font-extrabold text-tbh12b-fill bg-transparent transition-colors duration-[250ms] delay-[140ms] before:content-[''] before:absolute before:inset-0 before:z-0 before:bg-tbh12b-fill before:scale-y-0 before:origin-bottom before:transition-transform before:duration-[380ms] before:ease-[cubic-bezier(.4,0,.2,1)] hover:text-tbh12b-onfill hover:before:scale-y-100 focus-visible:text-tbh12b-onfill focus-visible:before:scale-y-100 focus-visible:outline-3 focus-visible:outline-tbh12b-fill focus-visible:outline-offset-[3px] motion-reduce:transition-none motion-reduce:before:transition-none">
<span class="relative z-[1]">Get the app</span>
</button>
</section>
<section class="tbh-12c flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh12-sans bg-tbh12c-stage" aria-label="Center-out fill button">
<button type="button" class="relative isolate overflow-hidden cursor-pointer rounded-full border-2 border-tbh12c-fill px-[42px] py-[17px] text-base font-extrabold text-tbh12c-fill bg-transparent transition-colors duration-[250ms] delay-[120ms] before:content-[''] before:absolute before:inset-0 before:z-0 before:bg-tbh12c-fill before:scale-x-0 before:origin-center before:transition-transform before:duration-400 before:ease-[cubic-bezier(.4,0,.2,1)] hover:text-tbh12c-onfill hover:before:scale-x-100 focus-visible:text-tbh12c-onfill focus-visible:before:scale-x-100 focus-visible:outline-3 focus-visible:outline-tbh12c-fill focus-visible:outline-offset-[3px] motion-reduce:transition-none motion-reduce:before:transition-none">
<span class="relative z-[1]">Start trial</span>
</button>
</section>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tbh12a-fill: #3f6ad0;
--color-tbh12a-fill: oklch(0.55 0.17 255);
--color-tbh12a-onfill: #ffffff;
--color-tbh12a-onfill: oklch(1 0 0);
--color-tbh12a-stage: #eef1f6;
--color-tbh12a-stage: oklch(0.95 0.008 255);
--color-tbh12b-fill: #4cb87a;
--color-tbh12b-fill: oklch(0.6 0.16 150);
--color-tbh12b-onfill: #04180d;
--color-tbh12b-onfill: oklch(0.2 0.05 150);
--color-tbh12b-stage: #0d160f;
--color-tbh12b-stage: oklch(0.16 0.02 150);
--color-tbh12c-fill: #d2543c;
--color-tbh12c-fill: oklch(0.58 0.18 20);
--color-tbh12c-onfill: #ffffff;
--color-tbh12c-onfill: oklch(1 0 0);
--color-tbh12c-stage: #1a0f10;
--color-tbh12c-stage: oklch(0.18 0.02 20);
--font-tbh12-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* Nothing stays CSS. Three directions, three origin utilities, one delayed colour swap. */
.tbh-12-group {
width: 100%;
min-height: 100svh;
}
.tbh-12a,
.tbh-12b,
.tbh-12c {
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 Sliding Background Color Wipe Button Effects
Source: https://codefronts.com/motion/tailwind-css-button-hover-effects/tailwind-sliding-background-color-wipe-button-effects/
The dependable one: a solid colour wipes in and the label colour swaps to stay readable — the most requested hover in professional product UIs, in three directions. The port's whole subtlety is one utility: delay-[120ms] on the label's colour transition.
## HTML
```html
<div class="tbh-12-group w-full min-h-svh flex flex-wrap">
<section class="tbh-12a flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh12-sans bg-tbh12a-stage" aria-label="Colour wipe button">
<button type="button" class="relative isolate overflow-hidden cursor-pointer rounded-[10px] border-2 border-tbh12a-fill px-[38px] py-4 text-base font-bold text-tbh12a-fill bg-transparent transition-colors duration-[250ms] delay-[120ms] before:content-[''] before:absolute before:inset-0 before:z-0 before:bg-tbh12a-fill before:scale-x-0 before:origin-left before:transition-transform before:duration-400 before:ease-[cubic-bezier(.4,0,.2,1)] hover:text-tbh12a-onfill hover:before:scale-x-100 focus-visible:text-tbh12a-onfill focus-visible:before:scale-x-100 focus-visible:outline-3 focus-visible:outline-tbh12a-fill focus-visible:outline-offset-[3px] motion-reduce:transition-none motion-reduce:before:transition-none">
<span class="relative z-[1]">Contact sales</span>
</button>
</section>
<section class="tbh-12b flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh12-sans bg-tbh12b-stage" aria-label="Bottom-up fill button">
<button type="button" class="relative isolate overflow-hidden cursor-pointer rounded-[10px] border-2 border-tbh12b-fill px-[42px] py-[17px] text-base font-extrabold text-tbh12b-fill bg-transparent transition-colors duration-[250ms] delay-[140ms] before:content-[''] before:absolute before:inset-0 before:z-0 before:bg-tbh12b-fill before:scale-y-0 before:origin-bottom before:transition-transform before:duration-[380ms] before:ease-[cubic-bezier(.4,0,.2,1)] hover:text-tbh12b-onfill hover:before:scale-y-100 focus-visible:text-tbh12b-onfill focus-visible:before:scale-y-100 focus-visible:outline-3 focus-visible:outline-tbh12b-fill focus-visible:outline-offset-[3px] motion-reduce:transition-none motion-reduce:before:transition-none">
<span class="relative z-[1]">Get the app</span>
</button>
</section>
<section class="tbh-12c flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh12-sans bg-tbh12c-stage" aria-label="Center-out fill button">
<button type="button" class="relative isolate overflow-hidden cursor-pointer rounded-full border-2 border-tbh12c-fill px-[42px] py-[17px] text-base font-extrabold text-tbh12c-fill bg-transparent transition-colors duration-[250ms] delay-[120ms] before:content-[''] before:absolute before:inset-0 before:z-0 before:bg-tbh12c-fill before:scale-x-0 before:origin-center before:transition-transform before:duration-400 before:ease-[cubic-bezier(.4,0,.2,1)] hover:text-tbh12c-onfill hover:before:scale-x-100 focus-visible:text-tbh12c-onfill focus-visible:before:scale-x-100 focus-visible:outline-3 focus-visible:outline-tbh12c-fill focus-visible:outline-offset-[3px] motion-reduce:transition-none motion-reduce:before:transition-none">
<span class="relative z-[1]">Start trial</span>
</button>
</section>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tbh12a-fill: #3f6ad0;
--color-tbh12a-fill: oklch(0.55 0.17 255);
--color-tbh12a-onfill: #ffffff;
--color-tbh12a-onfill: oklch(1 0 0);
--color-tbh12a-stage: #eef1f6;
--color-tbh12a-stage: oklch(0.95 0.008 255);
--color-tbh12b-fill: #4cb87a;
--color-tbh12b-fill: oklch(0.6 0.16 150);
--color-tbh12b-onfill: #04180d;
--color-tbh12b-onfill: oklch(0.2 0.05 150);
--color-tbh12b-stage: #0d160f;
--color-tbh12b-stage: oklch(0.16 0.02 150);
--color-tbh12c-fill: #d2543c;
--color-tbh12c-fill: oklch(0.58 0.18 20);
--color-tbh12c-onfill: #ffffff;
--color-tbh12c-onfill: oklch(1 0 0);
--color-tbh12c-stage: #1a0f10;
--color-tbh12c-stage: oklch(0.18 0.02 20);
--font-tbh12-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* Nothing stays CSS. Three directions, three origin utilities, one delayed colour swap. */
.tbh-12-group {
width: 100%;
min-height: 100svh;
}
.tbh-12a,
.tbh-12b,
.tbh-12c {
min-height: 100svh;
}
```How this works
Scale, never width. A before: layer at inset-0 scaled to 0 on one axis costs nothing to animate and reflows nothing; animating width would relayout the button on every frame. This is the demo everyone writes first and writes wrong.
The delay is the craft. Without delay-[120ms] the label turns white while the button is still mostly transparent, and for ~100ms the text is unreadable. With it, the colour swap lands just as the fill passes under the text. One utility, and it is the difference between this effect feeling cheap and feeling paid-for.
Three origins, three products. Left-to-right reads as progress, bottom-up as filling, centre-out as emphasis. Same twelve utilities otherwise — which is the argument for keeping all three in one entry.
Make it yours
- Direction: swap
before:origin-leftfororigin-right,origin-top, or a diagonal viabefore:skew-x-[-12deg]with a wider layer. - Wipe speed:
before:duration-400withease-[cubic-bezier(.4,0,.2,1)]. - Label flip timing:
delay-[120ms]— roughly a third of the wipe duration is the sweet spot. - Palette: each design's
--color-tbh12*-filland-onfillpair; the border, the fill, the ring and the hover ink all read them.
Gotchas — read before shipping
- Delay the label swap or the text is briefly unreadable against a half-filled button — the one real accessibility trap in this pattern.
- Both label colours must clear 4.5:1:
text-tbh12a-fillon the stage ANDtext-tbh12a-onfillon the fill. Checking only the resting state is the usual mistake. - Use
before:scale-x-0, notbefore:w-0. The visual result is identical and the cost is not. - Under
motion-reduce:the fill is pinned present (motion-reduce:hover:before:scale-x-100with transitions off), so the hover still communicates — it just does not travel.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
Transforms, pseudo-elements and transition-delay are universal; with CSS unavailable each button is a normal bordered CTA.