19 / 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 Split Reveal Curtain Button Effect
A theatrical CTA: on hover the surface splits into halves that slide apart, revealing a contrasting label behind — an outward curtain. Both front halves are one data-front attribute clipped twice, so the front copy can never disagree with itself.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<div class="tbh-19-group w-full min-h-svh flex flex-wrap">
<section class="tbh-19a flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh19-sans bg-tbh19a-stage" aria-label="Split reveal button">
<button type="button" data-front="Hover me" class="relative overflow-hidden cursor-pointer border-0 rounded-xl px-11 py-[18px] text-base font-extrabold text-tbh19a-backink bg-tbh19a-back focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-4 before:content-[attr(data-front)] before:absolute before:inset-0 before:grid before:place-items-center before:text-white before:bg-tbh19a-front before:z-[1] before:[clip-path:inset(0_0_calc(50%-1px)_0)] before:transition-[translate] before:duration-400 before:ease-[cubic-bezier(.4,0,.2,1)] after:content-[attr(data-front)] after:absolute after:inset-0 after:grid after:place-items-center after:text-white after:bg-tbh19a-front after:z-[1] after:[clip-path:inset(calc(50%-1px)_0_0_0)] after:transition-[translate] after:duration-400 after:ease-[cubic-bezier(.4,0,.2,1)] hover:before:-translate-y-full hover:after:translate-y-full focus-visible:before:-translate-y-full focus-visible:after:translate-y-full motion-reduce:before:transition-none motion-reduce:after:transition-none">
<span class="relative z-0">Let's go →</span>
</button>
</section>
<section class="tbh-19b flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh19-sans bg-tbh19b-stage" aria-label="Vertical split reveal button">
<button type="button" data-front="Reveal" class="relative overflow-hidden cursor-pointer border-0 rounded-xl px-[46px] py-[18px] text-base font-extrabold text-tbh19b-backink bg-tbh19b-back focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-4 before:content-[attr(data-front)] before:absolute before:inset-0 before:grid before:place-items-center before:text-white before:bg-tbh19b-front before:z-[1] before:[clip-path:inset(0_calc(50%-1px)_0_0)] before:transition-[translate] before:duration-400 before:ease-[cubic-bezier(.4,0,.2,1)] after:content-[attr(data-front)] after:absolute after:inset-0 after:grid after:place-items-center after:text-white after:bg-tbh19b-front after:z-[1] after:[clip-path:inset(0_0_0_calc(50%-1px))] after:transition-[translate] after:duration-400 after:ease-[cubic-bezier(.4,0,.2,1)] hover:before:-translate-x-full hover:after:translate-x-full focus-visible:before:-translate-x-full focus-visible:after:translate-x-full motion-reduce:before:transition-none motion-reduce:after:transition-none">
<span class="relative z-0">Unlock ✦</span>
</button>
</section>
</div><div class="tbh-19-group w-full min-h-svh flex flex-wrap">
<section class="tbh-19a flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh19-sans bg-tbh19a-stage" aria-label="Split reveal button">
<button type="button" data-front="Hover me" class="relative overflow-hidden cursor-pointer border-0 rounded-xl px-11 py-[18px] text-base font-extrabold text-tbh19a-backink bg-tbh19a-back focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-4 before:content-[attr(data-front)] before:absolute before:inset-0 before:grid before:place-items-center before:text-white before:bg-tbh19a-front before:z-[1] before:[clip-path:inset(0_0_calc(50%-1px)_0)] before:transition-[translate] before:duration-400 before:ease-[cubic-bezier(.4,0,.2,1)] after:content-[attr(data-front)] after:absolute after:inset-0 after:grid after:place-items-center after:text-white after:bg-tbh19a-front after:z-[1] after:[clip-path:inset(calc(50%-1px)_0_0_0)] after:transition-[translate] after:duration-400 after:ease-[cubic-bezier(.4,0,.2,1)] hover:before:-translate-y-full hover:after:translate-y-full focus-visible:before:-translate-y-full focus-visible:after:translate-y-full motion-reduce:before:transition-none motion-reduce:after:transition-none">
<span class="relative z-0">Let's go →</span>
</button>
</section>
<section class="tbh-19b flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh19-sans bg-tbh19b-stage" aria-label="Vertical split reveal button">
<button type="button" data-front="Reveal" class="relative overflow-hidden cursor-pointer border-0 rounded-xl px-[46px] py-[18px] text-base font-extrabold text-tbh19b-backink bg-tbh19b-back focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-4 before:content-[attr(data-front)] before:absolute before:inset-0 before:grid before:place-items-center before:text-white before:bg-tbh19b-front before:z-[1] before:[clip-path:inset(0_calc(50%-1px)_0_0)] before:transition-[translate] before:duration-400 before:ease-[cubic-bezier(.4,0,.2,1)] after:content-[attr(data-front)] after:absolute after:inset-0 after:grid after:place-items-center after:text-white after:bg-tbh19b-front after:z-[1] after:[clip-path:inset(0_0_0_calc(50%-1px))] after:transition-[translate] after:duration-400 after:ease-[cubic-bezier(.4,0,.2,1)] hover:before:-translate-x-full hover:after:translate-x-full focus-visible:before:-translate-x-full focus-visible:after:translate-x-full motion-reduce:before:transition-none motion-reduce:after:transition-none">
<span class="relative z-0">Unlock ✦</span>
</button>
</section>
</div>@import "tailwindcss";
@theme {
--color-tbh19a-front: #6a4fc4;
--color-tbh19a-front: oklch(0.5 0.16 285);
--color-tbh19a-back: #74dba0;
--color-tbh19a-back: oklch(0.82 0.17 145);
--color-tbh19a-backink: #04140b;
--color-tbh19a-backink: oklch(0.2 0.05 145);
--color-tbh19a-stage: #0d0b1a;
--color-tbh19a-stage: oklch(0.15 0.03 285);
--color-tbh19b-front: #b8503a;
--color-tbh19b-front: oklch(0.52 0.16 25);
--color-tbh19b-back: #dcae4e;
--color-tbh19b-back: oklch(0.8 0.15 85);
--color-tbh19b-backink: #1a1206;
--color-tbh19b-backink: oklch(0.22 0.04 85);
--color-tbh19b-stage: #160b0a;
--color-tbh19b-stage: oklch(0.17 0.02 25);
--font-tbh19-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* Nothing stays CSS: two clipped attr() halves and one translate each. */
.tbh-19-group {
width: 100%;
min-height: 100svh;
}
.tbh-19a,
.tbh-19b {
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tbh19a-front: #6a4fc4;
--color-tbh19a-front: oklch(0.5 0.16 285);
--color-tbh19a-back: #74dba0;
--color-tbh19a-back: oklch(0.82 0.17 145);
--color-tbh19a-backink: #04140b;
--color-tbh19a-backink: oklch(0.2 0.05 145);
--color-tbh19a-stage: #0d0b1a;
--color-tbh19a-stage: oklch(0.15 0.03 285);
--color-tbh19b-front: #b8503a;
--color-tbh19b-front: oklch(0.52 0.16 25);
--color-tbh19b-back: #dcae4e;
--color-tbh19b-back: oklch(0.8 0.15 85);
--color-tbh19b-backink: #1a1206;
--color-tbh19b-backink: oklch(0.22 0.04 85);
--color-tbh19b-stage: #160b0a;
--color-tbh19b-stage: oklch(0.17 0.02 25);
--font-tbh19-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* Nothing stays CSS: two clipped attr() halves and one translate each. */
.tbh-19-group {
width: 100%;
min-height: 100svh;
}
.tbh-19a,
.tbh-19b {
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-tbh19a-front: #6a4fc4;
--color-tbh19a-front: oklch(0.5 0.16 285);
--color-tbh19a-back: #74dba0;
--color-tbh19a-back: oklch(0.82 0.17 145);
--color-tbh19a-backink: #04140b;
--color-tbh19a-backink: oklch(0.2 0.05 145);
--color-tbh19a-stage: #0d0b1a;
--color-tbh19a-stage: oklch(0.15 0.03 285);
--color-tbh19b-front: #b8503a;
--color-tbh19b-front: oklch(0.52 0.16 25);
--color-tbh19b-back: #dcae4e;
--color-tbh19b-back: oklch(0.8 0.15 85);
--color-tbh19b-backink: #1a1206;
--color-tbh19b-backink: oklch(0.22 0.04 85);
--color-tbh19b-stage: #160b0a;
--color-tbh19b-stage: oklch(0.17 0.02 25);
--font-tbh19-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* Nothing stays CSS: two clipped attr() halves and one translate each. */
.tbh-19-group {
width: 100%;
min-height: 100svh;
}
.tbh-19a,
.tbh-19b {
min-height: 100svh;
}@import "tailwindcss";
@theme {
--color-tbh19a-front: #6a4fc4;
--color-tbh19a-front: oklch(0.5 0.16 285);
--color-tbh19a-back: #74dba0;
--color-tbh19a-back: oklch(0.82 0.17 145);
--color-tbh19a-backink: #04140b;
--color-tbh19a-backink: oklch(0.2 0.05 145);
--color-tbh19a-stage: #0d0b1a;
--color-tbh19a-stage: oklch(0.15 0.03 285);
--color-tbh19b-front: #b8503a;
--color-tbh19b-front: oklch(0.52 0.16 25);
--color-tbh19b-back: #dcae4e;
--color-tbh19b-back: oklch(0.8 0.15 85);
--color-tbh19b-backink: #1a1206;
--color-tbh19b-backink: oklch(0.22 0.04 85);
--color-tbh19b-stage: #160b0a;
--color-tbh19b-stage: oklch(0.17 0.02 25);
--font-tbh19-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* Nothing stays CSS: two clipped attr() halves and one translate each. */
.tbh-19-group {
width: 100%;
min-height: 100svh;
}
.tbh-19a,
.tbh-19b {
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.
[clip-path:inset(0_0_calc(50%-1px)_0)]shows thatcalc()survives the underscore escape intact, which is what makes the anti-seam overlap expressible as a utility at all.- The back colours were promoted to token pairs (
-backplus-backink) because the revealed surface needs its own ink decision — in the CSS original the back label's colour was a bare hex on the button rule, easy to miss when retheming. - UI change from the CSS original: both reveals now fire on
focus-visible:, so a keyboard user sees the curtain part rather than just a focus ring on a closed button.
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 Reveal Curtain Button Effect
Source: https://codefronts.com/motion/tailwind-css-button-hover-effects/tailwind-split-reveal-curtain-button-effect/
A theatrical CTA: on hover the surface splits into halves that slide apart, revealing a contrasting label behind — an outward curtain. Both front halves are one data-front attribute clipped twice, so the front copy can never disagree with itself.
## HTML
```html
<div class="tbh-19-group w-full min-h-svh flex flex-wrap">
<section class="tbh-19a flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh19-sans bg-tbh19a-stage" aria-label="Split reveal button">
<button type="button" data-front="Hover me" class="relative overflow-hidden cursor-pointer border-0 rounded-xl px-11 py-[18px] text-base font-extrabold text-tbh19a-backink bg-tbh19a-back focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-4 before:content-[attr(data-front)] before:absolute before:inset-0 before:grid before:place-items-center before:text-white before:bg-tbh19a-front before:z-[1] before:[clip-path:inset(0_0_calc(50%-1px)_0)] before:transition-[translate] before:duration-400 before:ease-[cubic-bezier(.4,0,.2,1)] after:content-[attr(data-front)] after:absolute after:inset-0 after:grid after:place-items-center after:text-white after:bg-tbh19a-front after:z-[1] after:[clip-path:inset(calc(50%-1px)_0_0_0)] after:transition-[translate] after:duration-400 after:ease-[cubic-bezier(.4,0,.2,1)] hover:before:-translate-y-full hover:after:translate-y-full focus-visible:before:-translate-y-full focus-visible:after:translate-y-full motion-reduce:before:transition-none motion-reduce:after:transition-none">
<span class="relative z-0">Let's go →</span>
</button>
</section>
<section class="tbh-19b flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh19-sans bg-tbh19b-stage" aria-label="Vertical split reveal button">
<button type="button" data-front="Reveal" class="relative overflow-hidden cursor-pointer border-0 rounded-xl px-[46px] py-[18px] text-base font-extrabold text-tbh19b-backink bg-tbh19b-back focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-4 before:content-[attr(data-front)] before:absolute before:inset-0 before:grid before:place-items-center before:text-white before:bg-tbh19b-front before:z-[1] before:[clip-path:inset(0_calc(50%-1px)_0_0)] before:transition-[translate] before:duration-400 before:ease-[cubic-bezier(.4,0,.2,1)] after:content-[attr(data-front)] after:absolute after:inset-0 after:grid after:place-items-center after:text-white after:bg-tbh19b-front after:z-[1] after:[clip-path:inset(0_0_0_calc(50%-1px))] after:transition-[translate] after:duration-400 after:ease-[cubic-bezier(.4,0,.2,1)] hover:before:-translate-x-full hover:after:translate-x-full focus-visible:before:-translate-x-full focus-visible:after:translate-x-full motion-reduce:before:transition-none motion-reduce:after:transition-none">
<span class="relative z-0">Unlock ✦</span>
</button>
</section>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tbh19a-front: #6a4fc4;
--color-tbh19a-front: oklch(0.5 0.16 285);
--color-tbh19a-back: #74dba0;
--color-tbh19a-back: oklch(0.82 0.17 145);
--color-tbh19a-backink: #04140b;
--color-tbh19a-backink: oklch(0.2 0.05 145);
--color-tbh19a-stage: #0d0b1a;
--color-tbh19a-stage: oklch(0.15 0.03 285);
--color-tbh19b-front: #b8503a;
--color-tbh19b-front: oklch(0.52 0.16 25);
--color-tbh19b-back: #dcae4e;
--color-tbh19b-back: oklch(0.8 0.15 85);
--color-tbh19b-backink: #1a1206;
--color-tbh19b-backink: oklch(0.22 0.04 85);
--color-tbh19b-stage: #160b0a;
--color-tbh19b-stage: oklch(0.17 0.02 25);
--font-tbh19-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* Nothing stays CSS: two clipped attr() halves and one translate each. */
.tbh-19-group {
width: 100%;
min-height: 100svh;
}
.tbh-19a,
.tbh-19b {
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 Split Reveal Curtain Button Effect
Source: https://codefronts.com/motion/tailwind-css-button-hover-effects/tailwind-split-reveal-curtain-button-effect/
A theatrical CTA: on hover the surface splits into halves that slide apart, revealing a contrasting label behind — an outward curtain. Both front halves are one data-front attribute clipped twice, so the front copy can never disagree with itself.
## HTML
```html
<div class="tbh-19-group w-full min-h-svh flex flex-wrap">
<section class="tbh-19a flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh19-sans bg-tbh19a-stage" aria-label="Split reveal button">
<button type="button" data-front="Hover me" class="relative overflow-hidden cursor-pointer border-0 rounded-xl px-11 py-[18px] text-base font-extrabold text-tbh19a-backink bg-tbh19a-back focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-4 before:content-[attr(data-front)] before:absolute before:inset-0 before:grid before:place-items-center before:text-white before:bg-tbh19a-front before:z-[1] before:[clip-path:inset(0_0_calc(50%-1px)_0)] before:transition-[translate] before:duration-400 before:ease-[cubic-bezier(.4,0,.2,1)] after:content-[attr(data-front)] after:absolute after:inset-0 after:grid after:place-items-center after:text-white after:bg-tbh19a-front after:z-[1] after:[clip-path:inset(calc(50%-1px)_0_0_0)] after:transition-[translate] after:duration-400 after:ease-[cubic-bezier(.4,0,.2,1)] hover:before:-translate-y-full hover:after:translate-y-full focus-visible:before:-translate-y-full focus-visible:after:translate-y-full motion-reduce:before:transition-none motion-reduce:after:transition-none">
<span class="relative z-0">Let's go →</span>
</button>
</section>
<section class="tbh-19b flex-[1_1_340px] min-w-[300px] grid place-items-center px-6 py-14 font-tbh19-sans bg-tbh19b-stage" aria-label="Vertical split reveal button">
<button type="button" data-front="Reveal" class="relative overflow-hidden cursor-pointer border-0 rounded-xl px-[46px] py-[18px] text-base font-extrabold text-tbh19b-backink bg-tbh19b-back focus-visible:outline-3 focus-visible:outline-white focus-visible:outline-offset-4 before:content-[attr(data-front)] before:absolute before:inset-0 before:grid before:place-items-center before:text-white before:bg-tbh19b-front before:z-[1] before:[clip-path:inset(0_calc(50%-1px)_0_0)] before:transition-[translate] before:duration-400 before:ease-[cubic-bezier(.4,0,.2,1)] after:content-[attr(data-front)] after:absolute after:inset-0 after:grid after:place-items-center after:text-white after:bg-tbh19b-front after:z-[1] after:[clip-path:inset(0_0_0_calc(50%-1px))] after:transition-[translate] after:duration-400 after:ease-[cubic-bezier(.4,0,.2,1)] hover:before:-translate-x-full hover:after:translate-x-full focus-visible:before:-translate-x-full focus-visible:after:translate-x-full motion-reduce:before:transition-none motion-reduce:after:transition-none">
<span class="relative z-0">Unlock ✦</span>
</button>
</section>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tbh19a-front: #6a4fc4;
--color-tbh19a-front: oklch(0.5 0.16 285);
--color-tbh19a-back: #74dba0;
--color-tbh19a-back: oklch(0.82 0.17 145);
--color-tbh19a-backink: #04140b;
--color-tbh19a-backink: oklch(0.2 0.05 145);
--color-tbh19a-stage: #0d0b1a;
--color-tbh19a-stage: oklch(0.15 0.03 285);
--color-tbh19b-front: #b8503a;
--color-tbh19b-front: oklch(0.52 0.16 25);
--color-tbh19b-back: #dcae4e;
--color-tbh19b-back: oklch(0.8 0.15 85);
--color-tbh19b-backink: #1a1206;
--color-tbh19b-backink: oklch(0.22 0.04 85);
--color-tbh19b-stage: #160b0a;
--color-tbh19b-stage: oklch(0.17 0.02 25);
--font-tbh19-sans: 'Space Grotesk', system-ui, sans-serif;
}
/* Nothing stays CSS: two clipped attr() halves and one translate each. */
.tbh-19-group {
width: 100%;
min-height: 100svh;
}
.tbh-19a,
.tbh-19b {
min-height: 100svh;
}
```How this works
The 1px overlap is the detail that matters. Each half is clipped to calc(50% - 1px) rather than a flat 50%, so the two windows overlap by 2px at the seam. Without it, subpixel rounding shows the back label as a bright hairline through the middle of a closed button.
One attribute, two halves. Both pseudo-elements read data-front, so the front label is a single source of truth. Editing the copy means editing one attribute, and the halves stay aligned because they are the same text at the same position, just clipped differently.
The back label is real text. It sits in the button, in the flow, announced normally — the curtain in front of it is decoration. That is what keeps a fairly theatrical effect completely uninteresting to a screen reader.
Make it yours
- Axis: design A parts vertically, design B horizontally. Swapping is two clip windows and two translate utilities.
- Palette:
--color-tbh19a-front(the curtain) and-back(what is revealed) — the contrast between them IS the effect. - Travel:
-translate-y-fullparts completely;-translate-y-3/4leaves a sliver of the curtain visible, which reads as a peek rather than a reveal. - Pace:
duration-400withease-[cubic-bezier(.4,0,.2,1)]; slower reads as a stage curtain, faster as a shutter.
Gotchas — read before shipping
- Clip to
calc(50% - 1px), never a flat 50% — the hairline seam is the bug everyone ships with this pattern. overflow-hiddenon the button clips the departing halves; without it they slide out over the page.- The back label must be legible against the back colour AND not merely inherit the front's ink — design A's back is light green with dark ink, which is a deliberate second pairing.
- The front halves are pseudo-elements and therefore silent to assistive tech. If the front copy carries meaning the back copy does not, that meaning is invisible to screen readers — keep them two phrasings of one action.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
clip-path: inset(), attr() content and transforms are Baseline; with CSS unavailable the back label renders as ordinary button text.