25 CSS Play / Pause Buttons04 / 25
Neumorphic Soft UI Media Control
Soft UI done properly: the button is extruded from the surface with a matched light/dark shadow pair, and pressing it swaps to inset shadows so the control physically sinks in. The demo also fixes neumorphism's fatal flaw — its notorious contrast failure — by keeping the icon and label at accessible contrast and adding a visible focus ring, so it looks like 2019's dream and passes 2027's audits.
Published
The code
<section class="pp-04" aria-label="Neumorphic soft UI media control demo">
<div class="pp-04__stage">
<div class="pp-04__panel">
<p class="pp-04__eyebrow">Now playing</p>
<p class="pp-04__title">Soft Machine</p>
<div class="pp-04__row">
<button class="pp-04__mini" type="button" aria-label="Previous track">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M7 6v12M19 6l-9 6 9 6z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/></svg>
</button>
<input class="pp-04__chk" type="checkbox" id="pp-04-toggle" aria-label="Play or pause Soft Machine">
<label class="pp-04__btn" for="pp-04-toggle">
<svg viewBox="0 0 24 24" width="26" height="26" aria-hidden="true">
<path class="pp-04__p" d="M8.6 5.4v13.2L19 12z" fill="currentColor"/>
<path class="pp-04__q" d="M8 5h3.1v14H8zM12.9 5H16v14h-3.1z" fill="currentColor"/>
</svg>
</label>
<button class="pp-04__mini" type="button" aria-label="Next track">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M17 6v12M5 6l9 6-9 6z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/></svg>
</button>
</div>
<span class="pp-04__track" aria-hidden="true"><b></b></span>
</div>
<p class="pp-04__chip" aria-hidden="true">dual box-shadow extrusion · inset on :checked · color-mix() derived light</p>
</div>
</section><section class="pp-04" aria-label="Neumorphic soft UI media control demo">
<div class="pp-04__stage">
<div class="pp-04__panel">
<p class="pp-04__eyebrow">Now playing</p>
<p class="pp-04__title">Soft Machine</p>
<div class="pp-04__row">
<button class="pp-04__mini" type="button" aria-label="Previous track">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M7 6v12M19 6l-9 6 9 6z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/></svg>
</button>
<input class="pp-04__chk" type="checkbox" id="pp-04-toggle" aria-label="Play or pause Soft Machine">
<label class="pp-04__btn" for="pp-04-toggle">
<svg viewBox="0 0 24 24" width="26" height="26" aria-hidden="true">
<path class="pp-04__p" d="M8.6 5.4v13.2L19 12z" fill="currentColor"/>
<path class="pp-04__q" d="M8 5h3.1v14H8zM12.9 5H16v14h-3.1z" fill="currentColor"/>
</svg>
</label>
<button class="pp-04__mini" type="button" aria-label="Next track">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M17 6v12M5 6l9 6-9 6z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/></svg>
</button>
</div>
<span class="pp-04__track" aria-hidden="true"><b></b></span>
</div>
<p class="pp-04__chip" aria-hidden="true">dual box-shadow extrusion · inset on :checked · color-mix() derived light</p>
</div>
</section>.pp-04,
.pp-04 *,
.pp-04 *::before,
.pp-04 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.pp-04 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--pp-04-bg);
--pp-04-surface: oklch(0.93 0.008 265);
--pp-04-bg: oklch(0.93 0.008 265);
--pp-04-dark: color-mix(in oklch,var(--pp-04-surface) 84%,black);
--pp-04-light: color-mix(in oklch,var(--pp-04-surface) 62%,white);
--pp-04-ink: oklch(0.34 0.02 265);
--pp-04-mut: oklch(0.56 0.015 265);
--pp-04-acc: oklch(0.58 0.15 275);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--pp-04-ink);
}
.pp-04__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: 26px;
padding: clamp(20px,5vw,56px);
}
.pp-04__panel {
display: grid;
justify-items: center;
gap: 6px;
width: min(100%,360px);
padding: 34px 30px 30px;
border-radius: 34px;
background: var(--pp-04-surface);
box-shadow: 14px 14px 32px var(--pp-04-dark),-14px -14px 32px var(--pp-04-light);
}
.pp-04__eyebrow {
font-size: 11px;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--pp-04-mut);
}
.pp-04__title {
font-size: 20px;
font-weight: 650;
letter-spacing: -.015em;
}
.pp-04__row {
display: flex;
align-items: center;
gap: 20px;
margin-top: 18px;
}
.pp-04__chk {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
clip-path: inset(50%);
}
.pp-04__btn {
display: grid;
place-items: center;
width: 88px;
height: 88px;
border-radius: 50%;
background: var(--pp-04-surface);
color: var(--pp-04-acc);
cursor: pointer;
box-shadow: 9px 9px 20px var(--pp-04-dark),-9px -9px 20px var(--pp-04-light),inset 0 0 0 1px color-mix(in oklch,var(--pp-04-dark) 45%,transparent);
transition: box-shadow .26s ease,color .26s,transform .26s;
}
.pp-04__btn:hover {
transform: translateY(-2px);
}
.pp-04__chk:checked+.pp-04__btn {
color: var(--pp-04-ink);
box-shadow: inset 7px 7px 15px var(--pp-04-dark),inset -7px -7px 15px var(--pp-04-light);
transform: none;
}
.pp-04__chk:focus-visible+.pp-04__btn {
outline: 3px solid var(--pp-04-acc);
outline-offset: 5px;
}
.pp-04__q {
display: none;
}
.pp-04__chk:checked+.pp-04__btn .pp-04__p {
display: none;
}
.pp-04__chk:checked+.pp-04__btn .pp-04__q {
display: block;
}
.pp-04__mini {
display: grid;
place-items: center;
width: 52px;
height: 52px;
border: 0;
border-radius: 50%;
background: var(--pp-04-surface);
color: var(--pp-04-mut);
cursor: pointer;
box-shadow: 6px 6px 14px var(--pp-04-dark),-6px -6px 14px var(--pp-04-light);
transition: box-shadow .22s,color .22s;
}
.pp-04__mini:hover {
color: var(--pp-04-acc);
}
.pp-04__mini:active {
box-shadow: inset 5px 5px 11px var(--pp-04-dark),inset -5px -5px 11px var(--pp-04-light);
}
.pp-04__mini:focus-visible {
outline: 3px solid var(--pp-04-acc);
outline-offset: 4px;
}
.pp-04__track {
display: block;
width: 100%;
height: 12px;
margin-top: 26px;
border-radius: 99px;
background: var(--pp-04-surface);
box-shadow: inset 4px 4px 9px var(--pp-04-dark),inset -4px -4px 9px var(--pp-04-light);
overflow: hidden;
padding: 3px;
}
.pp-04__track b {
display: block;
height: 100%;
width: 0;
border-radius: 99px;
background: var(--pp-04-acc);
animation: pp-04-fill 16s linear infinite;
animation-play-state: paused;
}
.pp-04__panel:has(.pp-04__chk:checked) .pp-04__track b {
animation-play-state: running;
}
@keyframes pp-04-fill {
from {
width: 0;
}
to {
width: 100%;
}
}
.pp-04__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--pp-04-mut);
padding: 8px 14px;
border-radius: 99px;
background: var(--pp-04-surface);
box-shadow: inset 3px 3px 7px var(--pp-04-dark),inset -3px -3px 7px var(--pp-04-light);
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.pp-04 * {
transition-duration: .01ms !important;
animation: none !important;
}
}.pp-04,
.pp-04 *,
.pp-04 *::before,
.pp-04 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.pp-04 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--pp-04-bg);
--pp-04-surface: oklch(0.93 0.008 265);
--pp-04-bg: oklch(0.93 0.008 265);
--pp-04-dark: color-mix(in oklch,var(--pp-04-surface) 84%,black);
--pp-04-light: color-mix(in oklch,var(--pp-04-surface) 62%,white);
--pp-04-ink: oklch(0.34 0.02 265);
--pp-04-mut: oklch(0.56 0.015 265);
--pp-04-acc: oklch(0.58 0.15 275);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--pp-04-ink);
}
.pp-04__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: 26px;
padding: clamp(20px,5vw,56px);
}
.pp-04__panel {
display: grid;
justify-items: center;
gap: 6px;
width: min(100%,360px);
padding: 34px 30px 30px;
border-radius: 34px;
background: var(--pp-04-surface);
box-shadow: 14px 14px 32px var(--pp-04-dark),-14px -14px 32px var(--pp-04-light);
}
.pp-04__eyebrow {
font-size: 11px;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--pp-04-mut);
}
.pp-04__title {
font-size: 20px;
font-weight: 650;
letter-spacing: -.015em;
}
.pp-04__row {
display: flex;
align-items: center;
gap: 20px;
margin-top: 18px;
}
.pp-04__chk {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
clip-path: inset(50%);
}
.pp-04__btn {
display: grid;
place-items: center;
width: 88px;
height: 88px;
border-radius: 50%;
background: var(--pp-04-surface);
color: var(--pp-04-acc);
cursor: pointer;
box-shadow: 9px 9px 20px var(--pp-04-dark),-9px -9px 20px var(--pp-04-light),inset 0 0 0 1px color-mix(in oklch,var(--pp-04-dark) 45%,transparent);
transition: box-shadow .26s ease,color .26s,transform .26s;
}
.pp-04__btn:hover {
transform: translateY(-2px);
}
.pp-04__chk:checked+.pp-04__btn {
color: var(--pp-04-ink);
box-shadow: inset 7px 7px 15px var(--pp-04-dark),inset -7px -7px 15px var(--pp-04-light);
transform: none;
}
.pp-04__chk:focus-visible+.pp-04__btn {
outline: 3px solid var(--pp-04-acc);
outline-offset: 5px;
}
.pp-04__q {
display: none;
}
.pp-04__chk:checked+.pp-04__btn .pp-04__p {
display: none;
}
.pp-04__chk:checked+.pp-04__btn .pp-04__q {
display: block;
}
.pp-04__mini {
display: grid;
place-items: center;
width: 52px;
height: 52px;
border: 0;
border-radius: 50%;
background: var(--pp-04-surface);
color: var(--pp-04-mut);
cursor: pointer;
box-shadow: 6px 6px 14px var(--pp-04-dark),-6px -6px 14px var(--pp-04-light);
transition: box-shadow .22s,color .22s;
}
.pp-04__mini:hover {
color: var(--pp-04-acc);
}
.pp-04__mini:active {
box-shadow: inset 5px 5px 11px var(--pp-04-dark),inset -5px -5px 11px var(--pp-04-light);
}
.pp-04__mini:focus-visible {
outline: 3px solid var(--pp-04-acc);
outline-offset: 4px;
}
.pp-04__track {
display: block;
width: 100%;
height: 12px;
margin-top: 26px;
border-radius: 99px;
background: var(--pp-04-surface);
box-shadow: inset 4px 4px 9px var(--pp-04-dark),inset -4px -4px 9px var(--pp-04-light);
overflow: hidden;
padding: 3px;
}
.pp-04__track b {
display: block;
height: 100%;
width: 0;
border-radius: 99px;
background: var(--pp-04-acc);
animation: pp-04-fill 16s linear infinite;
animation-play-state: paused;
}
.pp-04__panel:has(.pp-04__chk:checked) .pp-04__track b {
animation-play-state: running;
}
@keyframes pp-04-fill {
from {
width: 0;
}
to {
width: 100%;
}
}
.pp-04__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--pp-04-mut);
padding: 8px 14px;
border-radius: 99px;
background: var(--pp-04-surface);
box-shadow: inset 3px 3px 7px var(--pp-04-dark),inset -3px -3px 7px var(--pp-04-light);
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.pp-04 * {
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: Neumorphic Soft UI Media Control
Source: https://codefronts.com/components/css-play-pause-buttons/neumorphic-soft-ui-media-control/
Soft UI done properly: the button is extruded from the surface with a matched light/dark shadow pair, and pressing it swaps to inset shadows so the control physically sinks in. The demo also fixes neumorphism's fatal flaw — its notorious contrast failure — by keeping the icon and label at accessible contrast and adding a visible focus ring, so it looks like 2019's dream and passes 2027's audits.
## HTML
```html
<section class="pp-04" aria-label="Neumorphic soft UI media control demo">
<div class="pp-04__stage">
<div class="pp-04__panel">
<p class="pp-04__eyebrow">Now playing</p>
<p class="pp-04__title">Soft Machine</p>
<div class="pp-04__row">
<button class="pp-04__mini" type="button" aria-label="Previous track">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M7 6v12M19 6l-9 6 9 6z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/></svg>
</button>
<input class="pp-04__chk" type="checkbox" id="pp-04-toggle" aria-label="Play or pause Soft Machine">
<label class="pp-04__btn" for="pp-04-toggle">
<svg viewBox="0 0 24 24" width="26" height="26" aria-hidden="true">
<path class="pp-04__p" d="M8.6 5.4v13.2L19 12z" fill="currentColor"/>
<path class="pp-04__q" d="M8 5h3.1v14H8zM12.9 5H16v14h-3.1z" fill="currentColor"/>
</svg>
</label>
<button class="pp-04__mini" type="button" aria-label="Next track">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M17 6v12M5 6l9 6-9 6z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/></svg>
</button>
</div>
<span class="pp-04__track" aria-hidden="true"><b></b></span>
</div>
<p class="pp-04__chip" aria-hidden="true">dual box-shadow extrusion · inset on :checked · color-mix() derived light</p>
</div>
</section>
```
## CSS
```css
.pp-04,
.pp-04 *,
.pp-04 *::before,
.pp-04 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.pp-04 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--pp-04-bg);
--pp-04-surface: oklch(0.93 0.008 265);
--pp-04-bg: oklch(0.93 0.008 265);
--pp-04-dark: color-mix(in oklch,var(--pp-04-surface) 84%,black);
--pp-04-light: color-mix(in oklch,var(--pp-04-surface) 62%,white);
--pp-04-ink: oklch(0.34 0.02 265);
--pp-04-mut: oklch(0.56 0.015 265);
--pp-04-acc: oklch(0.58 0.15 275);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--pp-04-ink);
}
.pp-04__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: 26px;
padding: clamp(20px,5vw,56px);
}
.pp-04__panel {
display: grid;
justify-items: center;
gap: 6px;
width: min(100%,360px);
padding: 34px 30px 30px;
border-radius: 34px;
background: var(--pp-04-surface);
box-shadow: 14px 14px 32px var(--pp-04-dark),-14px -14px 32px var(--pp-04-light);
}
.pp-04__eyebrow {
font-size: 11px;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--pp-04-mut);
}
.pp-04__title {
font-size: 20px;
font-weight: 650;
letter-spacing: -.015em;
}
.pp-04__row {
display: flex;
align-items: center;
gap: 20px;
margin-top: 18px;
}
.pp-04__chk {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
clip-path: inset(50%);
}
.pp-04__btn {
display: grid;
place-items: center;
width: 88px;
height: 88px;
border-radius: 50%;
background: var(--pp-04-surface);
color: var(--pp-04-acc);
cursor: pointer;
box-shadow: 9px 9px 20px var(--pp-04-dark),-9px -9px 20px var(--pp-04-light),inset 0 0 0 1px color-mix(in oklch,var(--pp-04-dark) 45%,transparent);
transition: box-shadow .26s ease,color .26s,transform .26s;
}
.pp-04__btn:hover {
transform: translateY(-2px);
}
.pp-04__chk:checked+.pp-04__btn {
color: var(--pp-04-ink);
box-shadow: inset 7px 7px 15px var(--pp-04-dark),inset -7px -7px 15px var(--pp-04-light);
transform: none;
}
.pp-04__chk:focus-visible+.pp-04__btn {
outline: 3px solid var(--pp-04-acc);
outline-offset: 5px;
}
.pp-04__q {
display: none;
}
.pp-04__chk:checked+.pp-04__btn .pp-04__p {
display: none;
}
.pp-04__chk:checked+.pp-04__btn .pp-04__q {
display: block;
}
.pp-04__mini {
display: grid;
place-items: center;
width: 52px;
height: 52px;
border: 0;
border-radius: 50%;
background: var(--pp-04-surface);
color: var(--pp-04-mut);
cursor: pointer;
box-shadow: 6px 6px 14px var(--pp-04-dark),-6px -6px 14px var(--pp-04-light);
transition: box-shadow .22s,color .22s;
}
.pp-04__mini:hover {
color: var(--pp-04-acc);
}
.pp-04__mini:active {
box-shadow: inset 5px 5px 11px var(--pp-04-dark),inset -5px -5px 11px var(--pp-04-light);
}
.pp-04__mini:focus-visible {
outline: 3px solid var(--pp-04-acc);
outline-offset: 4px;
}
.pp-04__track {
display: block;
width: 100%;
height: 12px;
margin-top: 26px;
border-radius: 99px;
background: var(--pp-04-surface);
box-shadow: inset 4px 4px 9px var(--pp-04-dark),inset -4px -4px 9px var(--pp-04-light);
overflow: hidden;
padding: 3px;
}
.pp-04__track b {
display: block;
height: 100%;
width: 0;
border-radius: 99px;
background: var(--pp-04-acc);
animation: pp-04-fill 16s linear infinite;
animation-play-state: paused;
}
.pp-04__panel:has(.pp-04__chk:checked) .pp-04__track b {
animation-play-state: running;
}
@keyframes pp-04-fill {
from {
width: 0;
}
to {
width: 100%;
}
}
.pp-04__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--pp-04-mut);
padding: 8px 14px;
border-radius: 99px;
background: var(--pp-04-surface);
box-shadow: inset 3px 3px 7px var(--pp-04-dark),inset -3px -3px 7px var(--pp-04-light);
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.pp-04 * {
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: Neumorphic Soft UI Media Control
Source: https://codefronts.com/components/css-play-pause-buttons/neumorphic-soft-ui-media-control/
Soft UI done properly: the button is extruded from the surface with a matched light/dark shadow pair, and pressing it swaps to inset shadows so the control physically sinks in. The demo also fixes neumorphism's fatal flaw — its notorious contrast failure — by keeping the icon and label at accessible contrast and adding a visible focus ring, so it looks like 2019's dream and passes 2027's audits.
## HTML
```html
<section class="pp-04" aria-label="Neumorphic soft UI media control demo">
<div class="pp-04__stage">
<div class="pp-04__panel">
<p class="pp-04__eyebrow">Now playing</p>
<p class="pp-04__title">Soft Machine</p>
<div class="pp-04__row">
<button class="pp-04__mini" type="button" aria-label="Previous track">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M7 6v12M19 6l-9 6 9 6z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/></svg>
</button>
<input class="pp-04__chk" type="checkbox" id="pp-04-toggle" aria-label="Play or pause Soft Machine">
<label class="pp-04__btn" for="pp-04-toggle">
<svg viewBox="0 0 24 24" width="26" height="26" aria-hidden="true">
<path class="pp-04__p" d="M8.6 5.4v13.2L19 12z" fill="currentColor"/>
<path class="pp-04__q" d="M8 5h3.1v14H8zM12.9 5H16v14h-3.1z" fill="currentColor"/>
</svg>
</label>
<button class="pp-04__mini" type="button" aria-label="Next track">
<svg viewBox="0 0 24 24" width="18" height="18" aria-hidden="true"><path d="M17 6v12M5 6l9 6-9 6z" fill="none" stroke="currentColor" stroke-width="2" stroke-linejoin="round" stroke-linecap="round"/></svg>
</button>
</div>
<span class="pp-04__track" aria-hidden="true"><b></b></span>
</div>
<p class="pp-04__chip" aria-hidden="true">dual box-shadow extrusion · inset on :checked · color-mix() derived light</p>
</div>
</section>
```
## CSS
```css
.pp-04,
.pp-04 *,
.pp-04 *::before,
.pp-04 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.pp-04 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--pp-04-bg);
--pp-04-surface: oklch(0.93 0.008 265);
--pp-04-bg: oklch(0.93 0.008 265);
--pp-04-dark: color-mix(in oklch,var(--pp-04-surface) 84%,black);
--pp-04-light: color-mix(in oklch,var(--pp-04-surface) 62%,white);
--pp-04-ink: oklch(0.34 0.02 265);
--pp-04-mut: oklch(0.56 0.015 265);
--pp-04-acc: oklch(0.58 0.15 275);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--pp-04-ink);
}
.pp-04__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: 26px;
padding: clamp(20px,5vw,56px);
}
.pp-04__panel {
display: grid;
justify-items: center;
gap: 6px;
width: min(100%,360px);
padding: 34px 30px 30px;
border-radius: 34px;
background: var(--pp-04-surface);
box-shadow: 14px 14px 32px var(--pp-04-dark),-14px -14px 32px var(--pp-04-light);
}
.pp-04__eyebrow {
font-size: 11px;
letter-spacing: .16em;
text-transform: uppercase;
color: var(--pp-04-mut);
}
.pp-04__title {
font-size: 20px;
font-weight: 650;
letter-spacing: -.015em;
}
.pp-04__row {
display: flex;
align-items: center;
gap: 20px;
margin-top: 18px;
}
.pp-04__chk {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
clip-path: inset(50%);
}
.pp-04__btn {
display: grid;
place-items: center;
width: 88px;
height: 88px;
border-radius: 50%;
background: var(--pp-04-surface);
color: var(--pp-04-acc);
cursor: pointer;
box-shadow: 9px 9px 20px var(--pp-04-dark),-9px -9px 20px var(--pp-04-light),inset 0 0 0 1px color-mix(in oklch,var(--pp-04-dark) 45%,transparent);
transition: box-shadow .26s ease,color .26s,transform .26s;
}
.pp-04__btn:hover {
transform: translateY(-2px);
}
.pp-04__chk:checked+.pp-04__btn {
color: var(--pp-04-ink);
box-shadow: inset 7px 7px 15px var(--pp-04-dark),inset -7px -7px 15px var(--pp-04-light);
transform: none;
}
.pp-04__chk:focus-visible+.pp-04__btn {
outline: 3px solid var(--pp-04-acc);
outline-offset: 5px;
}
.pp-04__q {
display: none;
}
.pp-04__chk:checked+.pp-04__btn .pp-04__p {
display: none;
}
.pp-04__chk:checked+.pp-04__btn .pp-04__q {
display: block;
}
.pp-04__mini {
display: grid;
place-items: center;
width: 52px;
height: 52px;
border: 0;
border-radius: 50%;
background: var(--pp-04-surface);
color: var(--pp-04-mut);
cursor: pointer;
box-shadow: 6px 6px 14px var(--pp-04-dark),-6px -6px 14px var(--pp-04-light);
transition: box-shadow .22s,color .22s;
}
.pp-04__mini:hover {
color: var(--pp-04-acc);
}
.pp-04__mini:active {
box-shadow: inset 5px 5px 11px var(--pp-04-dark),inset -5px -5px 11px var(--pp-04-light);
}
.pp-04__mini:focus-visible {
outline: 3px solid var(--pp-04-acc);
outline-offset: 4px;
}
.pp-04__track {
display: block;
width: 100%;
height: 12px;
margin-top: 26px;
border-radius: 99px;
background: var(--pp-04-surface);
box-shadow: inset 4px 4px 9px var(--pp-04-dark),inset -4px -4px 9px var(--pp-04-light);
overflow: hidden;
padding: 3px;
}
.pp-04__track b {
display: block;
height: 100%;
width: 0;
border-radius: 99px;
background: var(--pp-04-acc);
animation: pp-04-fill 16s linear infinite;
animation-play-state: paused;
}
.pp-04__panel:has(.pp-04__chk:checked) .pp-04__track b {
animation-play-state: running;
}
@keyframes pp-04-fill {
from {
width: 0;
}
to {
width: 100%;
}
}
.pp-04__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--pp-04-mut);
padding: 8px 14px;
border-radius: 99px;
background: var(--pp-04-surface);
box-shadow: inset 3px 3px 7px var(--pp-04-dark),inset -3px -3px 7px var(--pp-04-light);
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.pp-04 * {
transition-duration: .01ms !important;
animation: none !important;
}
}
```How this works
Neumorphism is one idea: the element and its background are the same color, and two opposing shadows fake a light source at 135°.
--pp-04-surface:oklch(0.93 0.008 265);
--pp-04-dark:oklch(0.82 0.012 265); /* shadow side */
--pp-04-light:oklch(0.99 0.003 265); /* lit side */
.btn{ background:var(--pp-04-surface);
box-shadow: 9px 9px 20px var(--pp-04-dark),
-9px -9px 20px var(--pp-04-light); }
/* pressed = the SAME shadows, inset */
:checked + .btn{
box-shadow: inset 7px 7px 14px var(--pp-04-dark),
inset -7px -7px 14px var(--pp-04-light); }The lit and shadow colors must be derived from the surface, not invented: color-mix(in oklch, var(--pp-04-surface) 88%, black) and … 70%, white) keep the illusion consistent when you re-theme. Every element in the widget shares one light direction — mixing directions is what makes DIY soft UI look broken.
The play/pause icon is a nested <svg> cross-fade with a small rotate, and the transition on box-shadow is what sells the press: 260 ms is long enough to read as a physical depression, short enough to feel responsive. A second, smaller ring around the button (an inset hairline) adds the 'molded plastic' edge that pure two-shadow versions lack.
Make it yours
- Dark soft UI: set --pp-04-surface to oklch(0.28 0.015 265) and let the color-mix derivations do the rest — the same rules produce a convincing dark clay.
- Claymorphism: increase the blur radii to 40px, add
border-radius:38%and an inset highlight — softer, puffier, more toy-like. - Concave face: layer
background:linear-gradient(145deg, var(--pp-04-dark), var(--pp-04-light))on the pressed state for a dished surface. - Volume knob: the same shadow recipe on a circular div plus a
rotatedriven by an input[type=range] gives you a matching dial.
Gotchas — read before shipping
- Contrast is neumorphism's known failure (WCAG 1.4.11 needs 3:1 for UI boundaries). A pure same-color button on a same-color background has ~1:1 edge contrast — this demo adds a 1px inset hairline and a high-contrast icon to stay usable.
- Soft UI has no affordance: nothing says 'pressable'. Keep hover motion and a visible pressed state, and never rely on shadow alone to communicate the toggle's value — the icon changes too.
- Shadow colors must derive from the surface. Hardcoded #d1d9e6/#ffffff breaks the second someone changes the background.
- Two 20px blurred shadows on many elements is a real paint cost on mobile; keep neumorphic surfaces few and large.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 113+ | 111+ |
box-shadow and transitions are universal; only the oklch()/color-mix() token derivation sets the modern floor. Replace with hex pairs for full legacy support.