25 CSS Play / Pause Buttons22 / 25
Organic Blob Morph Play Button CSS
A button that never sits still: the eight-value border-radius drifts through four keyframes, so the shape breathes like a liquid droplet instead of a circle. On play the blob calms and tightens toward a circle — motion that means something. Zero SVG, zero filters, one keyframe, and it stays perfectly crisp at every size.
Published Updated
The code
<section class="pp-22" aria-label="Organic blob morph play button demo">
<div class="pp-22__stage">
<div class="pp-22__wrap">
<input class="pp-22-chk" type="checkbox" id="pp-22-toggle" aria-label="Play or pause the ambient set">
<label class="pp-22__btn" for="pp-22-toggle">
<span class="pp-22__ghost" aria-hidden="true"></span>
<span class="pp-22__blob" aria-hidden="true"></span>
<svg class="pp-22__icon" viewBox="0 0 24 24" width="32" height="32" aria-hidden="true">
<path class="pp-22__p" d="M9.2 5.6v12.8L19 12z" fill="currentColor"/>
<path class="pp-22__q" d="M8.8 5.6h3.2v12.8H8.8zM13 5.6h3.2v12.8H13z" fill="currentColor"/>
</svg>
</label>
<p class="pp-22__cap">Ambient set<span>8-value border-radius drift</span></p>
</div>
<p class="pp-22__chip" aria-hidden="true">border-radius: a b c d / e f g h · reversed second layer · calm on play</p>
</div>
</section><section class="pp-22" aria-label="Organic blob morph play button demo">
<div class="pp-22__stage">
<div class="pp-22__wrap">
<input class="pp-22-chk" type="checkbox" id="pp-22-toggle" aria-label="Play or pause the ambient set">
<label class="pp-22__btn" for="pp-22-toggle">
<span class="pp-22__ghost" aria-hidden="true"></span>
<span class="pp-22__blob" aria-hidden="true"></span>
<svg class="pp-22__icon" viewBox="0 0 24 24" width="32" height="32" aria-hidden="true">
<path class="pp-22__p" d="M9.2 5.6v12.8L19 12z" fill="currentColor"/>
<path class="pp-22__q" d="M8.8 5.6h3.2v12.8H8.8zM13 5.6h3.2v12.8H13z" fill="currentColor"/>
</svg>
</label>
<p class="pp-22__cap">Ambient set<span>8-value border-radius drift</span></p>
</div>
<p class="pp-22__chip" aria-hidden="true">border-radius: a b c d / e f g h · reversed second layer · calm on play</p>
</div>
</section>.pp-22,
.pp-22 *,
.pp-22 *::before,
.pp-22 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.pp-22 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--pp-22-bg);
--pp-22-bg: oklch(0.96 0.02 40);
--pp-22-ink: oklch(0.26 0.03 30);
--pp-22-mut: oklch(0.55 0.03 30);
--pp-22-c1: oklch(0.75 0.16 30);
--pp-22-c2: oklch(0.72 0.15 350);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--pp-22-ink);
}
.pp-22__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: 26px;
padding: clamp(20px,5vw,56px);
background: radial-gradient(70% 55% at 50% 45%,oklch(0.93 0.06 35/.9),transparent 72%);
}
.pp-22__wrap {
display: grid;
justify-items: center;
gap: 26px;
}
.pp-22-chk {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
clip-path: inset(50%);
}
.pp-22__btn {
position: relative;
display: grid;
place-items: center;
width: 170px;
height: 170px;
color: oklch(0.99 0 0);
cursor: pointer;
transition: scale .3s cubic-bezier(.34,1.4,.5,1);
}
.pp-22__btn:hover {
scale: 1.05;
}
.pp-22-chk:focus-visible+.pp-22__btn .pp-22__blob {
outline: 3px solid var(--pp-22-ink);
outline-offset: 6px;
}
.pp-22__blob,
.pp-22__ghost {
position: absolute;
inset: 0;
background: linear-gradient(135deg,var(--pp-22-c1),var(--pp-22-c2));
border-radius: 58% 42% 47% 53%/44% 52% 48% 56%;
animation: pp-22-blob 9s ease-in-out infinite;
transition: border-radius .8s ease,animation-duration .6s;
}
.pp-22__ghost {
inset: -14px;
opacity: .32;
filter: blur(10px);
animation-duration: 13s;
animation-direction: reverse;
}
.pp-22__icon {
position: relative;
z-index: 1;
filter: drop-shadow(0 2px 6px oklch(0.4 0.1 30/.4));
}
.pp-22__q {
display: none;
}
.pp-22-chk:checked+.pp-22__btn .pp-22__blob {
border-radius: 50%;
animation-duration: 22s;
}
.pp-22-chk:checked+.pp-22__btn .pp-22__ghost {
border-radius: 50%;
animation-duration: 26s;
opacity: .45;
}
.pp-22-chk:checked+.pp-22__btn .pp-22__p {
display: none;
}
.pp-22-chk:checked+.pp-22__btn .pp-22__q {
display: block;
}
@keyframes pp-22-blob {
0%,
100% {
border-radius: 58% 42% 47% 53%/44% 52% 48% 56%;
}
25% {
border-radius: 44% 56% 62% 38%/56% 40% 60% 44%;
}
50% {
border-radius: 52% 48% 38% 62%/62% 56% 44% 38%;
}
75% {
border-radius: 38% 62% 55% 45%/40% 46% 54% 60%;
}
}
.pp-22__cap {
text-align: center;
font-size: 15px;
font-weight: 640;
}
.pp-22__cap span {
display: block;
margin-top: 4px;
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
font-weight: 400;
color: var(--pp-22-mut);
}
.pp-22__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--pp-22-mut);
padding: 7px 14px;
border: 1px solid oklch(0.26 0.03 30/.16);
border-radius: 99px;
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.pp-22__blob,
.pp-22__ghost {
animation: none;
border-radius: 52% 48% 44% 56%/48% 52% 48% 52%;
}
}.pp-22,
.pp-22 *,
.pp-22 *::before,
.pp-22 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.pp-22 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--pp-22-bg);
--pp-22-bg: oklch(0.96 0.02 40);
--pp-22-ink: oklch(0.26 0.03 30);
--pp-22-mut: oklch(0.55 0.03 30);
--pp-22-c1: oklch(0.75 0.16 30);
--pp-22-c2: oklch(0.72 0.15 350);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--pp-22-ink);
}
.pp-22__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: 26px;
padding: clamp(20px,5vw,56px);
background: radial-gradient(70% 55% at 50% 45%,oklch(0.93 0.06 35/.9),transparent 72%);
}
.pp-22__wrap {
display: grid;
justify-items: center;
gap: 26px;
}
.pp-22-chk {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
clip-path: inset(50%);
}
.pp-22__btn {
position: relative;
display: grid;
place-items: center;
width: 170px;
height: 170px;
color: oklch(0.99 0 0);
cursor: pointer;
transition: scale .3s cubic-bezier(.34,1.4,.5,1);
}
.pp-22__btn:hover {
scale: 1.05;
}
.pp-22-chk:focus-visible+.pp-22__btn .pp-22__blob {
outline: 3px solid var(--pp-22-ink);
outline-offset: 6px;
}
.pp-22__blob,
.pp-22__ghost {
position: absolute;
inset: 0;
background: linear-gradient(135deg,var(--pp-22-c1),var(--pp-22-c2));
border-radius: 58% 42% 47% 53%/44% 52% 48% 56%;
animation: pp-22-blob 9s ease-in-out infinite;
transition: border-radius .8s ease,animation-duration .6s;
}
.pp-22__ghost {
inset: -14px;
opacity: .32;
filter: blur(10px);
animation-duration: 13s;
animation-direction: reverse;
}
.pp-22__icon {
position: relative;
z-index: 1;
filter: drop-shadow(0 2px 6px oklch(0.4 0.1 30/.4));
}
.pp-22__q {
display: none;
}
.pp-22-chk:checked+.pp-22__btn .pp-22__blob {
border-radius: 50%;
animation-duration: 22s;
}
.pp-22-chk:checked+.pp-22__btn .pp-22__ghost {
border-radius: 50%;
animation-duration: 26s;
opacity: .45;
}
.pp-22-chk:checked+.pp-22__btn .pp-22__p {
display: none;
}
.pp-22-chk:checked+.pp-22__btn .pp-22__q {
display: block;
}
@keyframes pp-22-blob {
0%,
100% {
border-radius: 58% 42% 47% 53%/44% 52% 48% 56%;
}
25% {
border-radius: 44% 56% 62% 38%/56% 40% 60% 44%;
}
50% {
border-radius: 52% 48% 38% 62%/62% 56% 44% 38%;
}
75% {
border-radius: 38% 62% 55% 45%/40% 46% 54% 60%;
}
}
.pp-22__cap {
text-align: center;
font-size: 15px;
font-weight: 640;
}
.pp-22__cap span {
display: block;
margin-top: 4px;
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
font-weight: 400;
color: var(--pp-22-mut);
}
.pp-22__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--pp-22-mut);
padding: 7px 14px;
border: 1px solid oklch(0.26 0.03 30/.16);
border-radius: 99px;
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.pp-22__blob,
.pp-22__ghost {
animation: none;
border-radius: 52% 48% 44% 56%/48% 52% 48% 52%;
}
}Here's a working CSS Play / Pause Button 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: Organic Blob Morph Play Button CSS
Source: https://codefronts.com/components/css-play-pause-buttons/liquid-drop/
A button that never sits still: the eight-value border-radius drifts through four keyframes, so the shape breathes like a liquid droplet instead of a circle. On play the blob calms and tightens toward a circle — motion that means something. Zero SVG, zero filters, one keyframe, and it stays perfectly crisp at every size.
## HTML
```html
<section class="pp-22" aria-label="Organic blob morph play button demo">
<div class="pp-22__stage">
<div class="pp-22__wrap">
<input class="pp-22-chk" type="checkbox" id="pp-22-toggle" aria-label="Play or pause the ambient set">
<label class="pp-22__btn" for="pp-22-toggle">
<span class="pp-22__ghost" aria-hidden="true"></span>
<span class="pp-22__blob" aria-hidden="true"></span>
<svg class="pp-22__icon" viewBox="0 0 24 24" width="32" height="32" aria-hidden="true">
<path class="pp-22__p" d="M9.2 5.6v12.8L19 12z" fill="currentColor"/>
<path class="pp-22__q" d="M8.8 5.6h3.2v12.8H8.8zM13 5.6h3.2v12.8H13z" fill="currentColor"/>
</svg>
</label>
<p class="pp-22__cap">Ambient set<span>8-value border-radius drift</span></p>
</div>
<p class="pp-22__chip" aria-hidden="true">border-radius: a b c d / e f g h · reversed second layer · calm on play</p>
</div>
</section>
```
## CSS
```css
.pp-22,
.pp-22 *,
.pp-22 *::before,
.pp-22 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.pp-22 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--pp-22-bg);
--pp-22-bg: oklch(0.96 0.02 40);
--pp-22-ink: oklch(0.26 0.03 30);
--pp-22-mut: oklch(0.55 0.03 30);
--pp-22-c1: oklch(0.75 0.16 30);
--pp-22-c2: oklch(0.72 0.15 350);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--pp-22-ink);
}
.pp-22__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: 26px;
padding: clamp(20px,5vw,56px);
background: radial-gradient(70% 55% at 50% 45%,oklch(0.93 0.06 35/.9),transparent 72%);
}
.pp-22__wrap {
display: grid;
justify-items: center;
gap: 26px;
}
.pp-22-chk {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
clip-path: inset(50%);
}
.pp-22__btn {
position: relative;
display: grid;
place-items: center;
width: 170px;
height: 170px;
color: oklch(0.99 0 0);
cursor: pointer;
transition: scale .3s cubic-bezier(.34,1.4,.5,1);
}
.pp-22__btn:hover {
scale: 1.05;
}
.pp-22-chk:focus-visible+.pp-22__btn .pp-22__blob {
outline: 3px solid var(--pp-22-ink);
outline-offset: 6px;
}
.pp-22__blob,
.pp-22__ghost {
position: absolute;
inset: 0;
background: linear-gradient(135deg,var(--pp-22-c1),var(--pp-22-c2));
border-radius: 58% 42% 47% 53%/44% 52% 48% 56%;
animation: pp-22-blob 9s ease-in-out infinite;
transition: border-radius .8s ease,animation-duration .6s;
}
.pp-22__ghost {
inset: -14px;
opacity: .32;
filter: blur(10px);
animation-duration: 13s;
animation-direction: reverse;
}
.pp-22__icon {
position: relative;
z-index: 1;
filter: drop-shadow(0 2px 6px oklch(0.4 0.1 30/.4));
}
.pp-22__q {
display: none;
}
.pp-22-chk:checked+.pp-22__btn .pp-22__blob {
border-radius: 50%;
animation-duration: 22s;
}
.pp-22-chk:checked+.pp-22__btn .pp-22__ghost {
border-radius: 50%;
animation-duration: 26s;
opacity: .45;
}
.pp-22-chk:checked+.pp-22__btn .pp-22__p {
display: none;
}
.pp-22-chk:checked+.pp-22__btn .pp-22__q {
display: block;
}
@keyframes pp-22-blob {
0%,
100% {
border-radius: 58% 42% 47% 53%/44% 52% 48% 56%;
}
25% {
border-radius: 44% 56% 62% 38%/56% 40% 60% 44%;
}
50% {
border-radius: 52% 48% 38% 62%/62% 56% 44% 38%;
}
75% {
border-radius: 38% 62% 55% 45%/40% 46% 54% 60%;
}
}
.pp-22__cap {
text-align: center;
font-size: 15px;
font-weight: 640;
}
.pp-22__cap span {
display: block;
margin-top: 4px;
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
font-weight: 400;
color: var(--pp-22-mut);
}
.pp-22__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--pp-22-mut);
padding: 7px 14px;
border: 1px solid oklch(0.26 0.03 30/.16);
border-radius: 99px;
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.pp-22__blob,
.pp-22__ghost {
animation: none;
border-radius: 52% 48% 44% 56%/48% 52% 48% 52%;
}
}
```Here's a working CSS Play / Pause Button 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: Organic Blob Morph Play Button CSS
Source: https://codefronts.com/components/css-play-pause-buttons/liquid-drop/
A button that never sits still: the eight-value border-radius drifts through four keyframes, so the shape breathes like a liquid droplet instead of a circle. On play the blob calms and tightens toward a circle — motion that means something. Zero SVG, zero filters, one keyframe, and it stays perfectly crisp at every size.
## HTML
```html
<section class="pp-22" aria-label="Organic blob morph play button demo">
<div class="pp-22__stage">
<div class="pp-22__wrap">
<input class="pp-22-chk" type="checkbox" id="pp-22-toggle" aria-label="Play or pause the ambient set">
<label class="pp-22__btn" for="pp-22-toggle">
<span class="pp-22__ghost" aria-hidden="true"></span>
<span class="pp-22__blob" aria-hidden="true"></span>
<svg class="pp-22__icon" viewBox="0 0 24 24" width="32" height="32" aria-hidden="true">
<path class="pp-22__p" d="M9.2 5.6v12.8L19 12z" fill="currentColor"/>
<path class="pp-22__q" d="M8.8 5.6h3.2v12.8H8.8zM13 5.6h3.2v12.8H13z" fill="currentColor"/>
</svg>
</label>
<p class="pp-22__cap">Ambient set<span>8-value border-radius drift</span></p>
</div>
<p class="pp-22__chip" aria-hidden="true">border-radius: a b c d / e f g h · reversed second layer · calm on play</p>
</div>
</section>
```
## CSS
```css
.pp-22,
.pp-22 *,
.pp-22 *::before,
.pp-22 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.pp-22 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--pp-22-bg);
--pp-22-bg: oklch(0.96 0.02 40);
--pp-22-ink: oklch(0.26 0.03 30);
--pp-22-mut: oklch(0.55 0.03 30);
--pp-22-c1: oklch(0.75 0.16 30);
--pp-22-c2: oklch(0.72 0.15 350);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--pp-22-ink);
}
.pp-22__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: 26px;
padding: clamp(20px,5vw,56px);
background: radial-gradient(70% 55% at 50% 45%,oklch(0.93 0.06 35/.9),transparent 72%);
}
.pp-22__wrap {
display: grid;
justify-items: center;
gap: 26px;
}
.pp-22-chk {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
clip-path: inset(50%);
}
.pp-22__btn {
position: relative;
display: grid;
place-items: center;
width: 170px;
height: 170px;
color: oklch(0.99 0 0);
cursor: pointer;
transition: scale .3s cubic-bezier(.34,1.4,.5,1);
}
.pp-22__btn:hover {
scale: 1.05;
}
.pp-22-chk:focus-visible+.pp-22__btn .pp-22__blob {
outline: 3px solid var(--pp-22-ink);
outline-offset: 6px;
}
.pp-22__blob,
.pp-22__ghost {
position: absolute;
inset: 0;
background: linear-gradient(135deg,var(--pp-22-c1),var(--pp-22-c2));
border-radius: 58% 42% 47% 53%/44% 52% 48% 56%;
animation: pp-22-blob 9s ease-in-out infinite;
transition: border-radius .8s ease,animation-duration .6s;
}
.pp-22__ghost {
inset: -14px;
opacity: .32;
filter: blur(10px);
animation-duration: 13s;
animation-direction: reverse;
}
.pp-22__icon {
position: relative;
z-index: 1;
filter: drop-shadow(0 2px 6px oklch(0.4 0.1 30/.4));
}
.pp-22__q {
display: none;
}
.pp-22-chk:checked+.pp-22__btn .pp-22__blob {
border-radius: 50%;
animation-duration: 22s;
}
.pp-22-chk:checked+.pp-22__btn .pp-22__ghost {
border-radius: 50%;
animation-duration: 26s;
opacity: .45;
}
.pp-22-chk:checked+.pp-22__btn .pp-22__p {
display: none;
}
.pp-22-chk:checked+.pp-22__btn .pp-22__q {
display: block;
}
@keyframes pp-22-blob {
0%,
100% {
border-radius: 58% 42% 47% 53%/44% 52% 48% 56%;
}
25% {
border-radius: 44% 56% 62% 38%/56% 40% 60% 44%;
}
50% {
border-radius: 52% 48% 38% 62%/62% 56% 44% 38%;
}
75% {
border-radius: 38% 62% 55% 45%/40% 46% 54% 60%;
}
}
.pp-22__cap {
text-align: center;
font-size: 15px;
font-weight: 640;
}
.pp-22__cap span {
display: block;
margin-top: 4px;
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
font-weight: 400;
color: var(--pp-22-mut);
}
.pp-22__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--pp-22-mut);
padding: 7px 14px;
border: 1px solid oklch(0.26 0.03 30/.16);
border-radius: 99px;
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.pp-22__blob,
.pp-22__ghost {
animation: none;
border-radius: 52% 48% 44% 56%/48% 52% 48% 52%;
}
}
```How this works
border-radius takes eight values — four horizontal radii, then four vertical — and every one of them interpolates. Animate them out of sync and you get organic motion:
@keyframes pp-22-blob{
0%,100%{ border-radius:58% 42% 47% 53% / 44% 52% 48% 56% }
25% { border-radius:44% 56% 62% 38% / 56% 40% 60% 44% }
50% { border-radius:52% 48% 38% 62% / 62% 56% 44% 38% }
75% { border-radius:38% 62% 55% 45% / 40% 46% 54% 60% } }
.blob{ animation:pp-22-blob 9s ease-in-out infinite; }The numbers are not random: each pair should stay near 50% and each keyframe should keep the four horizontal values summing to roughly 200%, otherwise the shape collapses into a lens or a teardrop. Slow it right down — 9 seconds — because organic motion that reads as 'alive' is always slower than UI motion.
Two layers deepen it: a second blob behind the first, running the same keyframe in reverse at a different duration, offset and blurred, so the silhouette shifts continuously. On :checked both settle toward border-radius:50% and the animation slows to a near stop — the visual language of 'now focused on playback'.
Make it yours
- Gooey merge: put two blobs in a container with
filter:blur(12px) contrast(22)for the classic metaball effect (heavier — measure it). - Hover excitement: shorten the duration to 4s on :hover for a shape that reacts to attention.
- Gradient skin: a 135° linear-gradient inside the blob shifts as the silhouette moves, which reads as translucency.
- Ambient card: the same keyframe on a large low-opacity background shape makes a beautiful section backdrop.
Gotchas — read before shipping
- Values must stay in a sane band (roughly 35–65%). Push past that and the blob pinches into shapes that read as broken.
- ease-in-out per keyframe is what makes it flow; linear reveals the four stops and the loop becomes obvious.
- border-radius animation is a paint-level change — cheap for one or two elements, expensive if you blob a whole grid.
- Text inside a morphing blob visibly shifts. Keep the icon centred and small, or counter-animate it.
- Under prefers-reduced-motion, freeze the shape at a pleasant keyframe rather than dropping to a plain circle — you keep the design language without the motion.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 113+ | 111+ |
Eight-value border-radius animation works in every engine (Chrome 4, Safari 5, Firefox 4). Only the oklch() tokens set the modern floor.