13 / 51
All 51 in 51 Tailwind CSS Buttons
- 01Tailwind Submit Button (Form Primary)
- 02Tailwind Primary and Secondary Button Pair
- 03Tailwind Ghost / Outline Button
- 04Tailwind Icon-Only Button with Accessible Tooltip
- 05Tailwind Gradient CTA Button
- 06Tailwind Loading Spinner Button (State Transition)
- 07Tailwind Disabled Button State (Full State Reference)
- 08Tailwind Button Group Component (Segmented Radio-Like)
- 09Tailwind Add to Cart Button
- 10Tailwind Download Progress Button
- 11Tailwind Like Heart Button
- 12Tailwind Confirm Delete Button
- 13Tailwind Play Pause Button
- 14Tailwind Subscribe Confetti Button
- 15Tailwind Magnetic Mercury Ripple Button
- 16Tailwind Brutalist Glitch Button
- 17Tailwind Neon Plasma Burst Button
- 18Tailwind Clay Press Button
- 19Tailwind Editorial Ink Spread Button
- 20Tailwind Particle Shard Burst Button
- 21Tailwind Brushed Brass Plate Button
- 22Tailwind Washi Paper Crane Button
- 23Tailwind Frosted Ice Cube Button
- 24Tailwind Vintage Typewriter Key Button
- 25Tailwind Lacquered Walnut Wood Button
- 26Tailwind Pharma Gel Capsule Button
- 27Tailwind Aged Copper Coin Button
- 28Tailwind Origami Fortune Cookie Button
- 29Tailwind Holographic Foil Sticker Button
- 30Tailwind Brutalist Concrete Slab Button
- 31Tailwind Mercury Bead Morph Button
- 32Tailwind Soft Pill Modern SaaS Button
- 33Tailwind Masking Tape Label Button
- 34Tailwind Obsidian Glass Shard Button
- 35Tailwind Wine Velvet Ribbon Button
- 36Tailwind LED Segment Display Button
- 37Tailwind Cassette Mixtape Label Button
- 38Tailwind Chalkboard Menu Button
- 39Tailwind Vinyl Record Sleeve Ripple Button
- 40Tailwind Polaroid Snapshot Button
- 41Tailwind Airline Boarding Pass Button
- 42Tailwind Aged Brass Subway Token Button
- 43Tailwind Vintage Rx Pharmacy Label Button
- 44Tailwind Bakelite Radio Dial Toggle Button
- 45Tailwind Regal Postage Stamp Button
- 46Tailwind Victorian Cigar Band Foil Button
- 47Tailwind Vintage Calculator Key Button
- 48Tailwind Toolbox Latch Spotlight Button
- 49Tailwind Receipt Paper Total Count-Up Button
- 50Tailwind IBM Punch Card Magnetic Follow Button
- 51Tailwind Brass Periscope Lens Button
Tailwind Play Pause Button
One shape that morphs between a play triangle and two pause bars — no icon swap, no SVG. Both states are clip-path: polygon() with matching point counts, and Tailwind carries them as arbitrary utilities, which puts the play polygon and the pause polygon on the same line of markup for the first time.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<div class="tb-13 w-full min-h-svh grid place-items-center font-tb13-sans p-[clamp(1.5rem,6vw,4rem)]">
<label class="group relative cursor-pointer grid place-items-center size-22 rounded-full bg-[linear-gradient(160deg,var(--color-tb13-surface-lift),var(--color-tb13-surface))] shadow-[inset_0_1px_0_var(--color-tb13-sheen),0_18px_40px_-18px_var(--color-tb13-glow)] transition-[scale,box-shadow] duration-[400ms] ease-tb13-out [-webkit-tap-highlight-color:transparent] hover:scale-105 active:scale-95 has-focus-visible:outline-2 has-focus-visible:outline-offset-[6px] has-focus-visible:outline-tb13-accent has-checked:shadow-[inset_0_1px_0_var(--color-tb13-sheen),0_22px_50px_-18px_var(--color-tb13-glow-2)] motion-reduce:transition-none motion-reduce:hover:scale-100">
<input type="checkbox" aria-label="Play or pause the episode" class="sr-only">
<span aria-hidden="true" class="absolute -inset-2 rounded-full opacity-[0.28] bg-[conic-gradient(from_0deg,transparent_0_55%,var(--color-tb13-accent-2),var(--color-tb13-accent),transparent)] [mask-image:radial-gradient(farthest-side,transparent_calc(100%-2.5px),#000_calc(100%-2.5px))] transition-opacity duration-[400ms] group-has-checked:opacity-100 group-has-checked:animate-tb13-spin motion-reduce:group-has-checked:animate-none"></span>
<span aria-hidden="true" class="relative w-[42%] h-[42%] translate-x-[4%] transition-transform duration-500 ease-tb13-out group-has-checked:translate-x-0 motion-reduce:transition-none">
<span class="absolute inset-0 bg-tb13-ink transition-[clip-path] duration-500 ease-tb13-out [clip-path:polygon(22%_6%,50%_24%,50%_76%,22%_94%)] group-has-checked:[clip-path:polygon(14%_6%,39%_6%,39%_94%,14%_94%)] motion-reduce:transition-none"></span>
<span class="absolute inset-0 bg-tb13-ink transition-[clip-path] duration-500 ease-tb13-out [clip-path:polygon(50%_24%,90%_50%,90%_50%,50%_76%)] group-has-checked:[clip-path:polygon(61%_6%,86%_6%,86%_94%,61%_94%)] motion-reduce:transition-none"></span>
</span>
</label>
</div><div class="tb-13 w-full min-h-svh grid place-items-center font-tb13-sans p-[clamp(1.5rem,6vw,4rem)]">
<label class="group relative cursor-pointer grid place-items-center size-22 rounded-full bg-[linear-gradient(160deg,var(--color-tb13-surface-lift),var(--color-tb13-surface))] shadow-[inset_0_1px_0_var(--color-tb13-sheen),0_18px_40px_-18px_var(--color-tb13-glow)] transition-[scale,box-shadow] duration-[400ms] ease-tb13-out [-webkit-tap-highlight-color:transparent] hover:scale-105 active:scale-95 has-focus-visible:outline-2 has-focus-visible:outline-offset-[6px] has-focus-visible:outline-tb13-accent has-checked:shadow-[inset_0_1px_0_var(--color-tb13-sheen),0_22px_50px_-18px_var(--color-tb13-glow-2)] motion-reduce:transition-none motion-reduce:hover:scale-100">
<input type="checkbox" aria-label="Play or pause the episode" class="sr-only">
<span aria-hidden="true" class="absolute -inset-2 rounded-full opacity-[0.28] bg-[conic-gradient(from_0deg,transparent_0_55%,var(--color-tb13-accent-2),var(--color-tb13-accent),transparent)] [mask-image:radial-gradient(farthest-side,transparent_calc(100%-2.5px),#000_calc(100%-2.5px))] transition-opacity duration-[400ms] group-has-checked:opacity-100 group-has-checked:animate-tb13-spin motion-reduce:group-has-checked:animate-none"></span>
<span aria-hidden="true" class="relative w-[42%] h-[42%] translate-x-[4%] transition-transform duration-500 ease-tb13-out group-has-checked:translate-x-0 motion-reduce:transition-none">
<span class="absolute inset-0 bg-tb13-ink transition-[clip-path] duration-500 ease-tb13-out [clip-path:polygon(22%_6%,50%_24%,50%_76%,22%_94%)] group-has-checked:[clip-path:polygon(14%_6%,39%_6%,39%_94%,14%_94%)] motion-reduce:transition-none"></span>
<span class="absolute inset-0 bg-tb13-ink transition-[clip-path] duration-500 ease-tb13-out [clip-path:polygon(50%_24%,90%_50%,90%_50%,50%_76%)] group-has-checked:[clip-path:polygon(61%_6%,86%_6%,86%_94%,61%_94%)] motion-reduce:transition-none"></span>
</span>
</label>
</div>@import "tailwindcss";
@theme {
--color-tb13-bg: #0d0b14;
--color-tb13-bg: oklch(0.14 0.02 295);
--color-tb13-surface: #191426;
--color-tb13-surface: oklch(0.22 0.04 295);
--color-tb13-surface-lift: #221c33;
--color-tb13-surface-lift: oklch(0.27 0.045 295);
--color-tb13-ink: #f4f1ff;
--color-tb13-ink: oklch(0.96 0.015 295);
--color-tb13-accent: #c084fc;
--color-tb13-accent: oklch(0.78 0.16 305);
--color-tb13-accent-2: #38bdf8;
--color-tb13-accent-2: oklch(0.78 0.13 222);
--color-tb13-sheen: #ffffff33;
--color-tb13-glow: #c084fcb3;
--color-tb13-glow: oklch(0.78 0.16 305 / 0.7);
--color-tb13-glow-2: #38bdf8cc;
--color-tb13-glow-2: oklch(0.78 0.13 222 / 0.8);
--font-tb13-sans: "Outfit", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb13-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb13-spin: tb-13-spin 2.6s linear infinite;
}
@keyframes tb-13-spin {
to {
rotate: 1turn;
}
}
.tb-13 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(40rem 26rem at 50% 45%, color-mix(in oklab, var(--color-tb13-accent) 16%, transparent), transparent 70%), var(--color-tb13-bg);
}
/* Safari ≤15.3 mask shim. */
.tb-13 [class*="mask-image"] {
-webkit-mask-image: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 2.5px));
}
@media (prefers-color-scheme: light) {
.tb-13:not([data-theme="dark"]) {
--color-tb13-bg: oklch(0.95 0.02 295);
--color-tb13-surface: #ffffff;
--color-tb13-surface-lift: oklch(0.99 0.004 295);
--color-tb13-ink: oklch(0.22 0.06 295);
--color-tb13-accent: oklch(0.55 0.23 295);
--color-tb13-accent-2: oklch(0.55 0.13 235);
}
}
.tb-13[data-theme="light"] {
--color-tb13-bg: oklch(0.95 0.02 295);
--color-tb13-surface: #ffffff;
--color-tb13-surface-lift: oklch(0.99 0.004 295);
--color-tb13-ink: oklch(0.22 0.06 295);
--color-tb13-accent: oklch(0.55 0.23 295);
}
@media (forced-colors: active) {
.tb-13 label {
border: 1px solid ButtonText;
background: ButtonFace;
}
.tb-13 label span span {
background: ButtonText;
}
.tb-13 label > span:first-of-type {
display: none;
}
}@import "tailwindcss";
@theme {
--color-tb13-bg: #0d0b14;
--color-tb13-bg: oklch(0.14 0.02 295);
--color-tb13-surface: #191426;
--color-tb13-surface: oklch(0.22 0.04 295);
--color-tb13-surface-lift: #221c33;
--color-tb13-surface-lift: oklch(0.27 0.045 295);
--color-tb13-ink: #f4f1ff;
--color-tb13-ink: oklch(0.96 0.015 295);
--color-tb13-accent: #c084fc;
--color-tb13-accent: oklch(0.78 0.16 305);
--color-tb13-accent-2: #38bdf8;
--color-tb13-accent-2: oklch(0.78 0.13 222);
--color-tb13-sheen: #ffffff33;
--color-tb13-glow: #c084fcb3;
--color-tb13-glow: oklch(0.78 0.16 305 / 0.7);
--color-tb13-glow-2: #38bdf8cc;
--color-tb13-glow-2: oklch(0.78 0.13 222 / 0.8);
--font-tb13-sans: "Outfit", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb13-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb13-spin: tb-13-spin 2.6s linear infinite;
}
@keyframes tb-13-spin {
to {
rotate: 1turn;
}
}
.tb-13 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(40rem 26rem at 50% 45%, color-mix(in oklab, var(--color-tb13-accent) 16%, transparent), transparent 70%), var(--color-tb13-bg);
}
/* Safari ≤15.3 mask shim. */
.tb-13 [class*="mask-image"] {
-webkit-mask-image: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 2.5px));
}
@media (prefers-color-scheme: light) {
.tb-13:not([data-theme="dark"]) {
--color-tb13-bg: oklch(0.95 0.02 295);
--color-tb13-surface: #ffffff;
--color-tb13-surface-lift: oklch(0.99 0.004 295);
--color-tb13-ink: oklch(0.22 0.06 295);
--color-tb13-accent: oklch(0.55 0.23 295);
--color-tb13-accent-2: oklch(0.55 0.13 235);
}
}
.tb-13[data-theme="light"] {
--color-tb13-bg: oklch(0.95 0.02 295);
--color-tb13-surface: #ffffff;
--color-tb13-surface-lift: oklch(0.99 0.004 295);
--color-tb13-ink: oklch(0.22 0.06 295);
--color-tb13-accent: oklch(0.55 0.23 295);
}
@media (forced-colors: active) {
.tb-13 label {
border: 1px solid ButtonText;
background: ButtonFace;
}
.tb-13 label span span {
background: ButtonText;
}
.tb-13 label > span:first-of-type {
display: none;
}
}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-tb13-bg: #0d0b14;
--color-tb13-bg: oklch(0.14 0.02 295);
--color-tb13-surface: #191426;
--color-tb13-surface: oklch(0.22 0.04 295);
--color-tb13-surface-lift: #221c33;
--color-tb13-surface-lift: oklch(0.27 0.045 295);
--color-tb13-ink: #f4f1ff;
--color-tb13-ink: oklch(0.96 0.015 295);
--color-tb13-accent: #c084fc;
--color-tb13-accent: oklch(0.78 0.16 305);
--color-tb13-accent-2: #38bdf8;
--color-tb13-accent-2: oklch(0.78 0.13 222);
--color-tb13-sheen: #ffffff33;
--color-tb13-glow: #c084fcb3;
--color-tb13-glow: oklch(0.78 0.16 305 / 0.7);
--color-tb13-glow-2: #38bdf8cc;
--color-tb13-glow-2: oklch(0.78 0.13 222 / 0.8);
--font-tb13-sans: "Outfit", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb13-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb13-spin: tb-13-spin 2.6s linear infinite;
}
@keyframes tb-13-spin {
to {
rotate: 1turn;
}
}
.tb-13 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(40rem 26rem at 50% 45%, color-mix(in oklab, var(--color-tb13-accent) 16%, transparent), transparent 70%), var(--color-tb13-bg);
}
/* Safari ≤15.3 mask shim. */
.tb-13 [class*="mask-image"] {
-webkit-mask-image: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 2.5px));
}
@media (prefers-color-scheme: light) {
.tb-13:not([data-theme="dark"]) {
--color-tb13-bg: oklch(0.95 0.02 295);
--color-tb13-surface: #ffffff;
--color-tb13-surface-lift: oklch(0.99 0.004 295);
--color-tb13-ink: oklch(0.22 0.06 295);
--color-tb13-accent: oklch(0.55 0.23 295);
--color-tb13-accent-2: oklch(0.55 0.13 235);
}
}
.tb-13[data-theme="light"] {
--color-tb13-bg: oklch(0.95 0.02 295);
--color-tb13-surface: #ffffff;
--color-tb13-surface-lift: oklch(0.99 0.004 295);
--color-tb13-ink: oklch(0.22 0.06 295);
--color-tb13-accent: oklch(0.55 0.23 295);
}
@media (forced-colors: active) {
.tb-13 label {
border: 1px solid ButtonText;
background: ButtonFace;
}
.tb-13 label span span {
background: ButtonText;
}
.tb-13 label > span:first-of-type {
display: none;
}
}@import "tailwindcss";
@theme {
--color-tb13-bg: #0d0b14;
--color-tb13-bg: oklch(0.14 0.02 295);
--color-tb13-surface: #191426;
--color-tb13-surface: oklch(0.22 0.04 295);
--color-tb13-surface-lift: #221c33;
--color-tb13-surface-lift: oklch(0.27 0.045 295);
--color-tb13-ink: #f4f1ff;
--color-tb13-ink: oklch(0.96 0.015 295);
--color-tb13-accent: #c084fc;
--color-tb13-accent: oklch(0.78 0.16 305);
--color-tb13-accent-2: #38bdf8;
--color-tb13-accent-2: oklch(0.78 0.13 222);
--color-tb13-sheen: #ffffff33;
--color-tb13-glow: #c084fcb3;
--color-tb13-glow: oklch(0.78 0.16 305 / 0.7);
--color-tb13-glow-2: #38bdf8cc;
--color-tb13-glow-2: oklch(0.78 0.13 222 / 0.8);
--font-tb13-sans: "Outfit", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb13-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb13-spin: tb-13-spin 2.6s linear infinite;
}
@keyframes tb-13-spin {
to {
rotate: 1turn;
}
}
.tb-13 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(40rem 26rem at 50% 45%, color-mix(in oklab, var(--color-tb13-accent) 16%, transparent), transparent 70%), var(--color-tb13-bg);
}
/* Safari ≤15.3 mask shim. */
.tb-13 [class*="mask-image"] {
-webkit-mask-image: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 2.5px));
}
@media (prefers-color-scheme: light) {
.tb-13:not([data-theme="dark"]) {
--color-tb13-bg: oklch(0.95 0.02 295);
--color-tb13-surface: #ffffff;
--color-tb13-surface-lift: oklch(0.99 0.004 295);
--color-tb13-ink: oklch(0.22 0.06 295);
--color-tb13-accent: oklch(0.55 0.23 295);
--color-tb13-accent-2: oklch(0.55 0.13 235);
}
}
.tb-13[data-theme="light"] {
--color-tb13-bg: oklch(0.95 0.02 295);
--color-tb13-surface: #ffffff;
--color-tb13-surface-lift: oklch(0.99 0.004 295);
--color-tb13-ink: oklch(0.22 0.06 295);
--color-tb13-accent: oklch(0.55 0.23 295);
}
@media (forced-colors: active) {
.tb-13 label {
border: 1px solid ButtonText;
background: ButtonFace;
}
.tb-13 label span span {
background: ButtonText;
}
.tb-13 label > span:first-of-type {
display: none;
}
}<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=Outfit:wght@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=Outfit:wght@500;600;700&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Utilities: everything — both clip-path states, the ring, the mask, the spin, the optical offset and the shadows. CSS: one keyframe, the stage backdrop and the
-webkit-maskshim. - Four arbitrary
[clip-path:polygon(...)]values are long, and they are still the right call: the underscore escape carries the multi-value syntax intact, and putting the rest state and the morphed state side by side is how you see that the point counts match. That check was invisible in the CSS original. group-has-checked:replaces five:has(input:checked)descendant rules. The ring's spin, both polygons, the glyph offset and the shadow all read their own condition now.- A11y fix from the CSS original: the focus ring was drawn with
outline-offset:6pxon a:has(input:focus-visible)rule but the light-scheme accent (#7c3aedon#f4f1fb) sat at about 2.8:1 against the page. The light branch now usesoklch(0.55 0.23 295), ≈4.6:1, so the indicator is visible in both schemes.
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 Play Pause Button
Source: https://codefronts.com/components/tailwind-css-buttons/tailwind-play-pause-button/
One shape that morphs between a play triangle and two pause bars — no icon swap, no SVG. Both states are clip-path: polygon() with matching point counts, and Tailwind carries them as arbitrary utilities, which puts the play polygon and the pause polygon on the same line of markup for the first time.
## HTML
```html
<div class="tb-13 w-full min-h-svh grid place-items-center font-tb13-sans p-[clamp(1.5rem,6vw,4rem)]">
<label class="group relative cursor-pointer grid place-items-center size-22 rounded-full bg-[linear-gradient(160deg,var(--color-tb13-surface-lift),var(--color-tb13-surface))] shadow-[inset_0_1px_0_var(--color-tb13-sheen),0_18px_40px_-18px_var(--color-tb13-glow)] transition-[scale,box-shadow] duration-[400ms] ease-tb13-out [-webkit-tap-highlight-color:transparent] hover:scale-105 active:scale-95 has-focus-visible:outline-2 has-focus-visible:outline-offset-[6px] has-focus-visible:outline-tb13-accent has-checked:shadow-[inset_0_1px_0_var(--color-tb13-sheen),0_22px_50px_-18px_var(--color-tb13-glow-2)] motion-reduce:transition-none motion-reduce:hover:scale-100">
<input type="checkbox" aria-label="Play or pause the episode" class="sr-only">
<span aria-hidden="true" class="absolute -inset-2 rounded-full opacity-[0.28] bg-[conic-gradient(from_0deg,transparent_0_55%,var(--color-tb13-accent-2),var(--color-tb13-accent),transparent)] [mask-image:radial-gradient(farthest-side,transparent_calc(100%-2.5px),#000_calc(100%-2.5px))] transition-opacity duration-[400ms] group-has-checked:opacity-100 group-has-checked:animate-tb13-spin motion-reduce:group-has-checked:animate-none"></span>
<span aria-hidden="true" class="relative w-[42%] h-[42%] translate-x-[4%] transition-transform duration-500 ease-tb13-out group-has-checked:translate-x-0 motion-reduce:transition-none">
<span class="absolute inset-0 bg-tb13-ink transition-[clip-path] duration-500 ease-tb13-out [clip-path:polygon(22%_6%,50%_24%,50%_76%,22%_94%)] group-has-checked:[clip-path:polygon(14%_6%,39%_6%,39%_94%,14%_94%)] motion-reduce:transition-none"></span>
<span class="absolute inset-0 bg-tb13-ink transition-[clip-path] duration-500 ease-tb13-out [clip-path:polygon(50%_24%,90%_50%,90%_50%,50%_76%)] group-has-checked:[clip-path:polygon(61%_6%,86%_6%,86%_94%,61%_94%)] motion-reduce:transition-none"></span>
</span>
</label>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tb13-bg: #0d0b14;
--color-tb13-bg: oklch(0.14 0.02 295);
--color-tb13-surface: #191426;
--color-tb13-surface: oklch(0.22 0.04 295);
--color-tb13-surface-lift: #221c33;
--color-tb13-surface-lift: oklch(0.27 0.045 295);
--color-tb13-ink: #f4f1ff;
--color-tb13-ink: oklch(0.96 0.015 295);
--color-tb13-accent: #c084fc;
--color-tb13-accent: oklch(0.78 0.16 305);
--color-tb13-accent-2: #38bdf8;
--color-tb13-accent-2: oklch(0.78 0.13 222);
--color-tb13-sheen: #ffffff33;
--color-tb13-glow: #c084fcb3;
--color-tb13-glow: oklch(0.78 0.16 305 / 0.7);
--color-tb13-glow-2: #38bdf8cc;
--color-tb13-glow-2: oklch(0.78 0.13 222 / 0.8);
--font-tb13-sans: "Outfit", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb13-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb13-spin: tb-13-spin 2.6s linear infinite;
}
@keyframes tb-13-spin {
to {
rotate: 1turn;
}
}
.tb-13 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(40rem 26rem at 50% 45%, color-mix(in oklab, var(--color-tb13-accent) 16%, transparent), transparent 70%), var(--color-tb13-bg);
}
/* Safari ≤15.3 mask shim. */
.tb-13 [class*="mask-image"] {
-webkit-mask-image: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 2.5px));
}
@media (prefers-color-scheme: light) {
.tb-13:not([data-theme="dark"]) {
--color-tb13-bg: oklch(0.95 0.02 295);
--color-tb13-surface: #ffffff;
--color-tb13-surface-lift: oklch(0.99 0.004 295);
--color-tb13-ink: oklch(0.22 0.06 295);
--color-tb13-accent: oklch(0.55 0.23 295);
--color-tb13-accent-2: oklch(0.55 0.13 235);
}
}
.tb-13[data-theme="light"] {
--color-tb13-bg: oklch(0.95 0.02 295);
--color-tb13-surface: #ffffff;
--color-tb13-surface-lift: oklch(0.99 0.004 295);
--color-tb13-ink: oklch(0.22 0.06 295);
--color-tb13-accent: oklch(0.55 0.23 295);
}
@media (forced-colors: active) {
.tb-13 label {
border: 1px solid ButtonText;
background: ButtonFace;
}
.tb-13 label span span {
background: ButtonText;
}
.tb-13 label > span:first-of-type {
display: none;
}
}
```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 Play Pause Button
Source: https://codefronts.com/components/tailwind-css-buttons/tailwind-play-pause-button/
One shape that morphs between a play triangle and two pause bars — no icon swap, no SVG. Both states are clip-path: polygon() with matching point counts, and Tailwind carries them as arbitrary utilities, which puts the play polygon and the pause polygon on the same line of markup for the first time.
## HTML
```html
<div class="tb-13 w-full min-h-svh grid place-items-center font-tb13-sans p-[clamp(1.5rem,6vw,4rem)]">
<label class="group relative cursor-pointer grid place-items-center size-22 rounded-full bg-[linear-gradient(160deg,var(--color-tb13-surface-lift),var(--color-tb13-surface))] shadow-[inset_0_1px_0_var(--color-tb13-sheen),0_18px_40px_-18px_var(--color-tb13-glow)] transition-[scale,box-shadow] duration-[400ms] ease-tb13-out [-webkit-tap-highlight-color:transparent] hover:scale-105 active:scale-95 has-focus-visible:outline-2 has-focus-visible:outline-offset-[6px] has-focus-visible:outline-tb13-accent has-checked:shadow-[inset_0_1px_0_var(--color-tb13-sheen),0_22px_50px_-18px_var(--color-tb13-glow-2)] motion-reduce:transition-none motion-reduce:hover:scale-100">
<input type="checkbox" aria-label="Play or pause the episode" class="sr-only">
<span aria-hidden="true" class="absolute -inset-2 rounded-full opacity-[0.28] bg-[conic-gradient(from_0deg,transparent_0_55%,var(--color-tb13-accent-2),var(--color-tb13-accent),transparent)] [mask-image:radial-gradient(farthest-side,transparent_calc(100%-2.5px),#000_calc(100%-2.5px))] transition-opacity duration-[400ms] group-has-checked:opacity-100 group-has-checked:animate-tb13-spin motion-reduce:group-has-checked:animate-none"></span>
<span aria-hidden="true" class="relative w-[42%] h-[42%] translate-x-[4%] transition-transform duration-500 ease-tb13-out group-has-checked:translate-x-0 motion-reduce:transition-none">
<span class="absolute inset-0 bg-tb13-ink transition-[clip-path] duration-500 ease-tb13-out [clip-path:polygon(22%_6%,50%_24%,50%_76%,22%_94%)] group-has-checked:[clip-path:polygon(14%_6%,39%_6%,39%_94%,14%_94%)] motion-reduce:transition-none"></span>
<span class="absolute inset-0 bg-tb13-ink transition-[clip-path] duration-500 ease-tb13-out [clip-path:polygon(50%_24%,90%_50%,90%_50%,50%_76%)] group-has-checked:[clip-path:polygon(61%_6%,86%_6%,86%_94%,61%_94%)] motion-reduce:transition-none"></span>
</span>
</label>
</div>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tb13-bg: #0d0b14;
--color-tb13-bg: oklch(0.14 0.02 295);
--color-tb13-surface: #191426;
--color-tb13-surface: oklch(0.22 0.04 295);
--color-tb13-surface-lift: #221c33;
--color-tb13-surface-lift: oklch(0.27 0.045 295);
--color-tb13-ink: #f4f1ff;
--color-tb13-ink: oklch(0.96 0.015 295);
--color-tb13-accent: #c084fc;
--color-tb13-accent: oklch(0.78 0.16 305);
--color-tb13-accent-2: #38bdf8;
--color-tb13-accent-2: oklch(0.78 0.13 222);
--color-tb13-sheen: #ffffff33;
--color-tb13-glow: #c084fcb3;
--color-tb13-glow: oklch(0.78 0.16 305 / 0.7);
--color-tb13-glow-2: #38bdf8cc;
--color-tb13-glow-2: oklch(0.78 0.13 222 / 0.8);
--font-tb13-sans: "Outfit", ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
--ease-tb13-out: cubic-bezier(0.16, 1, 0.3, 1);
--animate-tb13-spin: tb-13-spin 2.6s linear infinite;
}
@keyframes tb-13-spin {
to {
rotate: 1turn;
}
}
.tb-13 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
background: radial-gradient(40rem 26rem at 50% 45%, color-mix(in oklab, var(--color-tb13-accent) 16%, transparent), transparent 70%), var(--color-tb13-bg);
}
/* Safari ≤15.3 mask shim. */
.tb-13 [class*="mask-image"] {
-webkit-mask-image: radial-gradient(farthest-side, transparent calc(100% - 2.5px), #000 calc(100% - 2.5px));
}
@media (prefers-color-scheme: light) {
.tb-13:not([data-theme="dark"]) {
--color-tb13-bg: oklch(0.95 0.02 295);
--color-tb13-surface: #ffffff;
--color-tb13-surface-lift: oklch(0.99 0.004 295);
--color-tb13-ink: oklch(0.22 0.06 295);
--color-tb13-accent: oklch(0.55 0.23 295);
--color-tb13-accent-2: oklch(0.55 0.13 235);
}
}
.tb-13[data-theme="light"] {
--color-tb13-bg: oklch(0.95 0.02 295);
--color-tb13-surface: #ffffff;
--color-tb13-surface-lift: oklch(0.99 0.004 295);
--color-tb13-ink: oklch(0.22 0.06 295);
--color-tb13-accent: oklch(0.55 0.23 295);
}
@media (forced-colors: active) {
.tb-13 label {
border: 1px solid ButtonText;
background: ButtonFace;
}
.tb-13 label span span {
background: ButtonText;
}
.tb-13 label > span:first-of-type {
display: none;
}
}
```How this works
Interpolating clip-path needs identical functions and identical point counts — and one continuous outline per element. Two bars cannot be one polygon, so the glyph is two halves that each morph between four points:
<!-- left half --> [clip-path:polygon(22%_6%,50%_24%,50%_76%,22%_94%)]
<!-- pause --> group-has-checked:[clip-path:polygon(14%_6%,39%_6%,39%_94%,14%_94%)]The right half doubles its apex vertex to keep four points in both states. In play mode the halves share the edge at 50%, so the triangle reads as one solid shape.
Both states on one element is the port's real gain. In CSS the pause polygons lived eight lines below the play polygons, under a different selector; here the morph is a single class list you can read end to end.
The control is a checkbox with an aria-label. For a real player, mirror the state onto the media element and keep the label current from JS — never both aria-pressed and a changing label.
Make it yours
- Resize through the button's
size-22: the glyph is percentage-based and follows. - Real progress instead of a decorative ring: feed a registered percentage into the conic gradient, as demo 10 does.
- Square media button:
rounded-[1.1rem]on the button. The glyph is unaffected. - Add a stop state with a four-point square polygon behind a second condition — it still interpolates.
- Morph speed:
duration-500is cinematic,duration-200suits a podcast app.
Gotchas — read before shipping
clip-pathonly animates between the same function with the same point count. Mixinginset()andpolygon(), or 3 points and 8, snaps instantly.- A single
polygon()cannot describe two bars: a self-intersecting outline renders a bowtie, becausenonzerofills the crossing. One element per bar. clip-pathcuts a shape from a coloured box — the half must have a background, or there is nothing to clip.- A play/pause control must announce its state. The checkbox carries it; with a
<button>usearia-pressedor a changing label, never both. - Centre the triangle optically, not mathematically — hence
translate-x-[4%]in the play state only.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
clip-path polygon interpolation is supported everywhere relevant; :has() (Chrome 105 / Safari 15.4 / Firefox 121) drives the toggle. Both sit below Tailwind v4's own floor.