25 CSS Play / Pause Buttons19 / 25
Floating Action Button (FAB) Media Player
A floating action button that expands into a mini player: press play and the 64px circle stretches into a pill containing artwork, title and a progress line — one element, one transition, no layout thrash. This is the pattern mobile apps use for a persistent 'now playing' affordance, and it is achievable with a single width/border-radius transition plus staggered child reveals.
Published Updated
The code
<section class="pp-19" aria-label="Floating action button media player demo">
<div class="pp-19__stage">
<div class="pp-19__sheet" aria-hidden="true">
<span class="pp-19__ghost"></span><span class="pp-19__ghost pp-19__ghost--s"></span><span class="pp-19__ghost"></span><span class="pp-19__ghost pp-19__ghost--s"></span>
</div>
<div class="pp-19__dock">
<input class="pp-19-chk" type="checkbox" id="pp-19-toggle" aria-label="Play or pause Weightless Cities">
<label class="pp-19__fab" for="pp-19-toggle">
<span class="pp-19__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="24" height="24">
<path class="pp-19__p" d="M9 5.6v12.8L18.8 12z" fill="currentColor"/>
<path class="pp-19__q" d="M8.6 5.6h3.1v12.8H8.6zM12.9 5.6H16v12.8h-3.1z" fill="currentColor"/>
</svg>
</span>
<span class="pp-19__detail">
<img class="pp-19__art" src="https://images.unsplash.com/photo-1459749411175-04bf5292ceea?q=80&w=200&auto=format&fit=crop" alt="" width="44" height="44" loading="lazy">
<span class="pp-19__text">
<b>Weightless Cities</b>
<em>Hollow Coast · playing now</em>
<span class="pp-19__line"><i></i></span>
</span>
</span>
</label>
</div>
<p class="pp-19__chip" aria-hidden="true">width + border-radius morph · asymmetric transition-delay · pure CSS state</p>
</div>
</section><section class="pp-19" aria-label="Floating action button media player demo">
<div class="pp-19__stage">
<div class="pp-19__sheet" aria-hidden="true">
<span class="pp-19__ghost"></span><span class="pp-19__ghost pp-19__ghost--s"></span><span class="pp-19__ghost"></span><span class="pp-19__ghost pp-19__ghost--s"></span>
</div>
<div class="pp-19__dock">
<input class="pp-19-chk" type="checkbox" id="pp-19-toggle" aria-label="Play or pause Weightless Cities">
<label class="pp-19__fab" for="pp-19-toggle">
<span class="pp-19__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="24" height="24">
<path class="pp-19__p" d="M9 5.6v12.8L18.8 12z" fill="currentColor"/>
<path class="pp-19__q" d="M8.6 5.6h3.1v12.8H8.6zM12.9 5.6H16v12.8h-3.1z" fill="currentColor"/>
</svg>
</span>
<span class="pp-19__detail">
<img class="pp-19__art" src="https://images.unsplash.com/photo-1459749411175-04bf5292ceea?q=80&w=200&auto=format&fit=crop" alt="" width="44" height="44" loading="lazy">
<span class="pp-19__text">
<b>Weightless Cities</b>
<em>Hollow Coast · playing now</em>
<span class="pp-19__line"><i></i></span>
</span>
</span>
</label>
</div>
<p class="pp-19__chip" aria-hidden="true">width + border-radius morph · asymmetric transition-delay · pure CSS state</p>
</div>
</section>.pp-19,
.pp-19 *,
.pp-19 *::before,
.pp-19 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.pp-19 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--pp-19-bg);
--pp-19-bg: oklch(0.96 0.008 285);
--pp-19-ink: oklch(0.24 0.02 285);
--pp-19-mut: oklch(0.56 0.02 285);
--pp-19-fab: oklch(0.55 0.19 285);
--pp-19-on: oklch(0.99 0 0);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--pp-19-ink);
}
.pp-19__stage {
position: relative;
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: 30px;
padding: clamp(20px,5vw,56px);
}
.pp-19__sheet {
display: grid;
gap: 14px;
width: min(100%,360px);
opacity: .5;
}
.pp-19__ghost {
display: block;
height: 14px;
border-radius: 99px;
background: oklch(0.86 0.012 285);
}
.pp-19__ghost--s {
width: 62%;
}
.pp-19__dock {
display: grid;
justify-items: center;
min-height: 76px;
align-content: center;
}
.pp-19-chk {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
clip-path: inset(50%);
}
.pp-19__fab {
display: flex;
align-items: center;
gap: 0;
width: 68px;
height: 68px;
padding: 10px;
border-radius: 50%;
background: var(--pp-19-fab);
color: var(--pp-19-on);
cursor: pointer;
overflow: hidden;
white-space: nowrap;
box-shadow: 0 10px 26px color-mix(in oklch,var(--pp-19-fab) 42%,transparent);
transition: width .5s cubic-bezier(.5,0,.15,1),border-radius .5s cubic-bezier(.5,0,.15,1),gap .5s,box-shadow .3s;
}
.pp-19__fab:hover {
box-shadow: 0 14px 34px color-mix(in oklch,var(--pp-19-fab) 52%,transparent);
}
.pp-19-chk:focus-visible+.pp-19__fab {
outline: 3px solid var(--pp-19-ink);
outline-offset: 4px;
}
.pp-19__icon {
display: grid;
place-items: center;
width: 48px;
height: 48px;
flex: none;
}
.pp-19__q {
display: none;
}
.pp-19__detail {
display: flex;
align-items: center;
gap: 12px;
opacity: 0;
translate: -10px 0;
transition: opacity .22s,translate .22s;
}
.pp-19__art {
width: 44px;
height: 44px;
border-radius: 12px;
object-fit: cover;
flex: none;
background: oklch(0.7 0.12 300);
}
.pp-19__text {
display: grid;
gap: 2px;
min-width: 0;
}
.pp-19__text b {
font-size: 14px;
font-weight: 660;
letter-spacing: -.01em;
}
.pp-19__text em {
font-size: 11.5px;
font-style: normal;
opacity: .8;
}
.pp-19__line {
display: block;
width: 150px;
height: 3px;
margin-top: 5px;
border-radius: 99px;
background: oklch(1 0 0/.3);
overflow: hidden;
}
.pp-19__line i {
display: block;
height: 100%;
width: 0;
background: var(--pp-19-on);
animation: pp-19-fill 24s linear infinite;
animation-play-state: paused;
}
.pp-19-chk:checked+.pp-19__fab {
width: min(92vw,344px);
border-radius: 34px;
gap: 6px;
}
.pp-19-chk:checked+.pp-19__fab .pp-19__detail {
opacity: 1;
translate: 0;
transition-delay: .22s;
}
.pp-19-chk:checked+.pp-19__fab .pp-19__p {
display: none;
}
.pp-19-chk:checked+.pp-19__fab .pp-19__q {
display: block;
}
.pp-19-chk:checked+.pp-19__fab .pp-19__line i {
animation-play-state: running;
}
@keyframes pp-19-fill {
from {
width: 0;
}
to {
width: 100%;
}
}
.pp-19__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--pp-19-mut);
padding: 7px 14px;
border: 1px solid oklch(0.24 0.02 285/.15);
border-radius: 99px;
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.pp-19 * {
transition-duration: .01ms !important;
animation: none !important;
}
}.pp-19,
.pp-19 *,
.pp-19 *::before,
.pp-19 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.pp-19 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--pp-19-bg);
--pp-19-bg: oklch(0.96 0.008 285);
--pp-19-ink: oklch(0.24 0.02 285);
--pp-19-mut: oklch(0.56 0.02 285);
--pp-19-fab: oklch(0.55 0.19 285);
--pp-19-on: oklch(0.99 0 0);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--pp-19-ink);
}
.pp-19__stage {
position: relative;
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: 30px;
padding: clamp(20px,5vw,56px);
}
.pp-19__sheet {
display: grid;
gap: 14px;
width: min(100%,360px);
opacity: .5;
}
.pp-19__ghost {
display: block;
height: 14px;
border-radius: 99px;
background: oklch(0.86 0.012 285);
}
.pp-19__ghost--s {
width: 62%;
}
.pp-19__dock {
display: grid;
justify-items: center;
min-height: 76px;
align-content: center;
}
.pp-19-chk {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
clip-path: inset(50%);
}
.pp-19__fab {
display: flex;
align-items: center;
gap: 0;
width: 68px;
height: 68px;
padding: 10px;
border-radius: 50%;
background: var(--pp-19-fab);
color: var(--pp-19-on);
cursor: pointer;
overflow: hidden;
white-space: nowrap;
box-shadow: 0 10px 26px color-mix(in oklch,var(--pp-19-fab) 42%,transparent);
transition: width .5s cubic-bezier(.5,0,.15,1),border-radius .5s cubic-bezier(.5,0,.15,1),gap .5s,box-shadow .3s;
}
.pp-19__fab:hover {
box-shadow: 0 14px 34px color-mix(in oklch,var(--pp-19-fab) 52%,transparent);
}
.pp-19-chk:focus-visible+.pp-19__fab {
outline: 3px solid var(--pp-19-ink);
outline-offset: 4px;
}
.pp-19__icon {
display: grid;
place-items: center;
width: 48px;
height: 48px;
flex: none;
}
.pp-19__q {
display: none;
}
.pp-19__detail {
display: flex;
align-items: center;
gap: 12px;
opacity: 0;
translate: -10px 0;
transition: opacity .22s,translate .22s;
}
.pp-19__art {
width: 44px;
height: 44px;
border-radius: 12px;
object-fit: cover;
flex: none;
background: oklch(0.7 0.12 300);
}
.pp-19__text {
display: grid;
gap: 2px;
min-width: 0;
}
.pp-19__text b {
font-size: 14px;
font-weight: 660;
letter-spacing: -.01em;
}
.pp-19__text em {
font-size: 11.5px;
font-style: normal;
opacity: .8;
}
.pp-19__line {
display: block;
width: 150px;
height: 3px;
margin-top: 5px;
border-radius: 99px;
background: oklch(1 0 0/.3);
overflow: hidden;
}
.pp-19__line i {
display: block;
height: 100%;
width: 0;
background: var(--pp-19-on);
animation: pp-19-fill 24s linear infinite;
animation-play-state: paused;
}
.pp-19-chk:checked+.pp-19__fab {
width: min(92vw,344px);
border-radius: 34px;
gap: 6px;
}
.pp-19-chk:checked+.pp-19__fab .pp-19__detail {
opacity: 1;
translate: 0;
transition-delay: .22s;
}
.pp-19-chk:checked+.pp-19__fab .pp-19__p {
display: none;
}
.pp-19-chk:checked+.pp-19__fab .pp-19__q {
display: block;
}
.pp-19-chk:checked+.pp-19__fab .pp-19__line i {
animation-play-state: running;
}
@keyframes pp-19-fill {
from {
width: 0;
}
to {
width: 100%;
}
}
.pp-19__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--pp-19-mut);
padding: 7px 14px;
border: 1px solid oklch(0.24 0.02 285/.15);
border-radius: 99px;
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.pp-19 * {
transition-duration: .01ms !important;
animation: none !important;
}
}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: Floating Action Button (FAB) Media Player
Source: https://codefronts.com/components/css-play-pause-buttons/floating-fab/
A floating action button that expands into a mini player: press play and the 64px circle stretches into a pill containing artwork, title and a progress line — one element, one transition, no layout thrash. This is the pattern mobile apps use for a persistent 'now playing' affordance, and it is achievable with a single width/border-radius transition plus staggered child reveals.
## HTML
```html
<section class="pp-19" aria-label="Floating action button media player demo">
<div class="pp-19__stage">
<div class="pp-19__sheet" aria-hidden="true">
<span class="pp-19__ghost"></span><span class="pp-19__ghost pp-19__ghost--s"></span><span class="pp-19__ghost"></span><span class="pp-19__ghost pp-19__ghost--s"></span>
</div>
<div class="pp-19__dock">
<input class="pp-19-chk" type="checkbox" id="pp-19-toggle" aria-label="Play or pause Weightless Cities">
<label class="pp-19__fab" for="pp-19-toggle">
<span class="pp-19__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="24" height="24">
<path class="pp-19__p" d="M9 5.6v12.8L18.8 12z" fill="currentColor"/>
<path class="pp-19__q" d="M8.6 5.6h3.1v12.8H8.6zM12.9 5.6H16v12.8h-3.1z" fill="currentColor"/>
</svg>
</span>
<span class="pp-19__detail">
<img class="pp-19__art" src="https://images.unsplash.com/photo-1459749411175-04bf5292ceea?q=80&w=200&auto=format&fit=crop" alt="" width="44" height="44" loading="lazy">
<span class="pp-19__text">
<b>Weightless Cities</b>
<em>Hollow Coast · playing now</em>
<span class="pp-19__line"><i></i></span>
</span>
</span>
</label>
</div>
<p class="pp-19__chip" aria-hidden="true">width + border-radius morph · asymmetric transition-delay · pure CSS state</p>
</div>
</section>
```
## CSS
```css
.pp-19,
.pp-19 *,
.pp-19 *::before,
.pp-19 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.pp-19 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--pp-19-bg);
--pp-19-bg: oklch(0.96 0.008 285);
--pp-19-ink: oklch(0.24 0.02 285);
--pp-19-mut: oklch(0.56 0.02 285);
--pp-19-fab: oklch(0.55 0.19 285);
--pp-19-on: oklch(0.99 0 0);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--pp-19-ink);
}
.pp-19__stage {
position: relative;
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: 30px;
padding: clamp(20px,5vw,56px);
}
.pp-19__sheet {
display: grid;
gap: 14px;
width: min(100%,360px);
opacity: .5;
}
.pp-19__ghost {
display: block;
height: 14px;
border-radius: 99px;
background: oklch(0.86 0.012 285);
}
.pp-19__ghost--s {
width: 62%;
}
.pp-19__dock {
display: grid;
justify-items: center;
min-height: 76px;
align-content: center;
}
.pp-19-chk {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
clip-path: inset(50%);
}
.pp-19__fab {
display: flex;
align-items: center;
gap: 0;
width: 68px;
height: 68px;
padding: 10px;
border-radius: 50%;
background: var(--pp-19-fab);
color: var(--pp-19-on);
cursor: pointer;
overflow: hidden;
white-space: nowrap;
box-shadow: 0 10px 26px color-mix(in oklch,var(--pp-19-fab) 42%,transparent);
transition: width .5s cubic-bezier(.5,0,.15,1),border-radius .5s cubic-bezier(.5,0,.15,1),gap .5s,box-shadow .3s;
}
.pp-19__fab:hover {
box-shadow: 0 14px 34px color-mix(in oklch,var(--pp-19-fab) 52%,transparent);
}
.pp-19-chk:focus-visible+.pp-19__fab {
outline: 3px solid var(--pp-19-ink);
outline-offset: 4px;
}
.pp-19__icon {
display: grid;
place-items: center;
width: 48px;
height: 48px;
flex: none;
}
.pp-19__q {
display: none;
}
.pp-19__detail {
display: flex;
align-items: center;
gap: 12px;
opacity: 0;
translate: -10px 0;
transition: opacity .22s,translate .22s;
}
.pp-19__art {
width: 44px;
height: 44px;
border-radius: 12px;
object-fit: cover;
flex: none;
background: oklch(0.7 0.12 300);
}
.pp-19__text {
display: grid;
gap: 2px;
min-width: 0;
}
.pp-19__text b {
font-size: 14px;
font-weight: 660;
letter-spacing: -.01em;
}
.pp-19__text em {
font-size: 11.5px;
font-style: normal;
opacity: .8;
}
.pp-19__line {
display: block;
width: 150px;
height: 3px;
margin-top: 5px;
border-radius: 99px;
background: oklch(1 0 0/.3);
overflow: hidden;
}
.pp-19__line i {
display: block;
height: 100%;
width: 0;
background: var(--pp-19-on);
animation: pp-19-fill 24s linear infinite;
animation-play-state: paused;
}
.pp-19-chk:checked+.pp-19__fab {
width: min(92vw,344px);
border-radius: 34px;
gap: 6px;
}
.pp-19-chk:checked+.pp-19__fab .pp-19__detail {
opacity: 1;
translate: 0;
transition-delay: .22s;
}
.pp-19-chk:checked+.pp-19__fab .pp-19__p {
display: none;
}
.pp-19-chk:checked+.pp-19__fab .pp-19__q {
display: block;
}
.pp-19-chk:checked+.pp-19__fab .pp-19__line i {
animation-play-state: running;
}
@keyframes pp-19-fill {
from {
width: 0;
}
to {
width: 100%;
}
}
.pp-19__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--pp-19-mut);
padding: 7px 14px;
border: 1px solid oklch(0.24 0.02 285/.15);
border-radius: 99px;
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.pp-19 * {
transition-duration: .01ms !important;
animation: none !important;
}
}
```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: Floating Action Button (FAB) Media Player
Source: https://codefronts.com/components/css-play-pause-buttons/floating-fab/
A floating action button that expands into a mini player: press play and the 64px circle stretches into a pill containing artwork, title and a progress line — one element, one transition, no layout thrash. This is the pattern mobile apps use for a persistent 'now playing' affordance, and it is achievable with a single width/border-radius transition plus staggered child reveals.
## HTML
```html
<section class="pp-19" aria-label="Floating action button media player demo">
<div class="pp-19__stage">
<div class="pp-19__sheet" aria-hidden="true">
<span class="pp-19__ghost"></span><span class="pp-19__ghost pp-19__ghost--s"></span><span class="pp-19__ghost"></span><span class="pp-19__ghost pp-19__ghost--s"></span>
</div>
<div class="pp-19__dock">
<input class="pp-19-chk" type="checkbox" id="pp-19-toggle" aria-label="Play or pause Weightless Cities">
<label class="pp-19__fab" for="pp-19-toggle">
<span class="pp-19__icon" aria-hidden="true">
<svg viewBox="0 0 24 24" width="24" height="24">
<path class="pp-19__p" d="M9 5.6v12.8L18.8 12z" fill="currentColor"/>
<path class="pp-19__q" d="M8.6 5.6h3.1v12.8H8.6zM12.9 5.6H16v12.8h-3.1z" fill="currentColor"/>
</svg>
</span>
<span class="pp-19__detail">
<img class="pp-19__art" src="https://images.unsplash.com/photo-1459749411175-04bf5292ceea?q=80&w=200&auto=format&fit=crop" alt="" width="44" height="44" loading="lazy">
<span class="pp-19__text">
<b>Weightless Cities</b>
<em>Hollow Coast · playing now</em>
<span class="pp-19__line"><i></i></span>
</span>
</span>
</label>
</div>
<p class="pp-19__chip" aria-hidden="true">width + border-radius morph · asymmetric transition-delay · pure CSS state</p>
</div>
</section>
```
## CSS
```css
.pp-19,
.pp-19 *,
.pp-19 *::before,
.pp-19 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.pp-19 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--pp-19-bg);
--pp-19-bg: oklch(0.96 0.008 285);
--pp-19-ink: oklch(0.24 0.02 285);
--pp-19-mut: oklch(0.56 0.02 285);
--pp-19-fab: oklch(0.55 0.19 285);
--pp-19-on: oklch(0.99 0 0);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--pp-19-ink);
}
.pp-19__stage {
position: relative;
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: 30px;
padding: clamp(20px,5vw,56px);
}
.pp-19__sheet {
display: grid;
gap: 14px;
width: min(100%,360px);
opacity: .5;
}
.pp-19__ghost {
display: block;
height: 14px;
border-radius: 99px;
background: oklch(0.86 0.012 285);
}
.pp-19__ghost--s {
width: 62%;
}
.pp-19__dock {
display: grid;
justify-items: center;
min-height: 76px;
align-content: center;
}
.pp-19-chk {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
clip-path: inset(50%);
}
.pp-19__fab {
display: flex;
align-items: center;
gap: 0;
width: 68px;
height: 68px;
padding: 10px;
border-radius: 50%;
background: var(--pp-19-fab);
color: var(--pp-19-on);
cursor: pointer;
overflow: hidden;
white-space: nowrap;
box-shadow: 0 10px 26px color-mix(in oklch,var(--pp-19-fab) 42%,transparent);
transition: width .5s cubic-bezier(.5,0,.15,1),border-radius .5s cubic-bezier(.5,0,.15,1),gap .5s,box-shadow .3s;
}
.pp-19__fab:hover {
box-shadow: 0 14px 34px color-mix(in oklch,var(--pp-19-fab) 52%,transparent);
}
.pp-19-chk:focus-visible+.pp-19__fab {
outline: 3px solid var(--pp-19-ink);
outline-offset: 4px;
}
.pp-19__icon {
display: grid;
place-items: center;
width: 48px;
height: 48px;
flex: none;
}
.pp-19__q {
display: none;
}
.pp-19__detail {
display: flex;
align-items: center;
gap: 12px;
opacity: 0;
translate: -10px 0;
transition: opacity .22s,translate .22s;
}
.pp-19__art {
width: 44px;
height: 44px;
border-radius: 12px;
object-fit: cover;
flex: none;
background: oklch(0.7 0.12 300);
}
.pp-19__text {
display: grid;
gap: 2px;
min-width: 0;
}
.pp-19__text b {
font-size: 14px;
font-weight: 660;
letter-spacing: -.01em;
}
.pp-19__text em {
font-size: 11.5px;
font-style: normal;
opacity: .8;
}
.pp-19__line {
display: block;
width: 150px;
height: 3px;
margin-top: 5px;
border-radius: 99px;
background: oklch(1 0 0/.3);
overflow: hidden;
}
.pp-19__line i {
display: block;
height: 100%;
width: 0;
background: var(--pp-19-on);
animation: pp-19-fill 24s linear infinite;
animation-play-state: paused;
}
.pp-19-chk:checked+.pp-19__fab {
width: min(92vw,344px);
border-radius: 34px;
gap: 6px;
}
.pp-19-chk:checked+.pp-19__fab .pp-19__detail {
opacity: 1;
translate: 0;
transition-delay: .22s;
}
.pp-19-chk:checked+.pp-19__fab .pp-19__p {
display: none;
}
.pp-19-chk:checked+.pp-19__fab .pp-19__q {
display: block;
}
.pp-19-chk:checked+.pp-19__fab .pp-19__line i {
animation-play-state: running;
}
@keyframes pp-19-fill {
from {
width: 0;
}
to {
width: 100%;
}
}
.pp-19__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--pp-19-mut);
padding: 7px 14px;
border: 1px solid oklch(0.24 0.02 285/.15);
border-radius: 99px;
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.pp-19 * {
transition-duration: .01ms !important;
animation: none !important;
}
}
```How this works
The FAB and the mini player are the same element. Transition width and border-radius together, and let the children fade in behind the growth:
.fab{ width:64px; border-radius:50%;
transition:width .5s cubic-bezier(.5,0,.15,1), border-radius .5s; }
:checked ~ .fab{ width:min(92vw,340px); border-radius:34px; }
.fab .detail{ opacity:0; translate:-8px 0;
transition:opacity .25s, translate .25s; } /* no delay closing */
:checked ~ .fab .detail{ opacity:1; translate:0;
transition-delay:.22s; } /* delay opening */The asymmetric delay is the craft: content appears after the container has made room (220 ms in), but disappears immediately when collapsing. Symmetric timing makes the text spill outside the shrinking pill — the single most common bug in expanding-FAB implementations.
white-space:nowrap plus overflow:hidden on the pill keeps the content from wrapping mid-transition, and the artwork uses flex:none so it never squashes. Everything is CSS state driven by a clipped checkbox, so the component works with JS disabled and keeps native focus behaviour.
Make it yours
- Anchor it: position:fixed with
inset:auto 20px 20px autoand a safe-area inset for a real app-shell FAB. - Speed dial: on expand, reveal secondary actions (queue, like, cast) as staggered children with increasing transition-delays.
- Auto-collapse: add a :hover/:focus-within rule that expands on pointer and collapses on leave, for desktop.
- Elevation: Material's rest/hover shadow pair (level 3 → level 4) is two box-shadow values on the same transition.
Gotchas — read before shipping
- Transitioning width is layout work, but on a single absolutely-positioned element it is cheap and gives you real content reflow. Don't try it on a list of many items.
- Delay only on the way in. Same-delay transitions cause the text to overflow the collapsing pill.
- min() with a viewport unit keeps the expanded pill inside small screens — a fixed 340px overflows a 320px phone.
- A FAB must not cover content permanently; leave bottom padding on the page equal to the FAB height plus its offset.
- Keep the accessible name stable ('Play') and let aria-pressed carry state, even though the visual expands (see demo 07).
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 113+ | 111+ |
Width/border-radius transitions and transition-delay are universal. min() needs Chrome 79 / Safari 13.1 / Firefox 75; independent translate needs Chrome 104 / Safari 14.1 / Firefox 72.