25 CSS Play / Pause Buttons15 / 25
Retro Arcade Play Pause Button CSS
A physical arcade key: a chunky cap sitting on a coloured plinth, with the whole thing travelling down into the plinth when pressed — the shadow shrinks by exactly the distance the cap moves, which is what makes the depth read as real. The icon is genuine pixel art drawn with a stepped clip-path polygon, so it stays crisp at any size without a single image.
Published Updated
The code
<section class="pp-15" aria-label="Retro arcade play pause button demo">
<div class="pp-15__stage">
<div class="pp-15__cabinet">
<p class="pp-15__marquee">INSERT COIN — SIDE B</p>
<div class="pp-15__screen" aria-hidden="true">
<span class="pp-15__scan"></span>
<p class="pp-15__ready">READY</p>
<p class="pp-15__go">PLAYING</p>
</div>
<input class="pp-15-chk" type="checkbox" id="pp-15-toggle" aria-label="Play or pause">
<label class="pp-15__cap" for="pp-15-toggle">
<span class="pp-15__px" aria-hidden="true"></span>
<span class="pp-15__bars" aria-hidden="true"><i></i><i></i></span>
</label>
<p class="pp-15__hint">PRESS TO START</p>
</div>
<p class="pp-15__chip" aria-hidden="true">travel + matching shadow shrink · stepped clip-path pixel art · 100ms linear</p>
</div>
</section><section class="pp-15" aria-label="Retro arcade play pause button demo">
<div class="pp-15__stage">
<div class="pp-15__cabinet">
<p class="pp-15__marquee">INSERT COIN — SIDE B</p>
<div class="pp-15__screen" aria-hidden="true">
<span class="pp-15__scan"></span>
<p class="pp-15__ready">READY</p>
<p class="pp-15__go">PLAYING</p>
</div>
<input class="pp-15-chk" type="checkbox" id="pp-15-toggle" aria-label="Play or pause">
<label class="pp-15__cap" for="pp-15-toggle">
<span class="pp-15__px" aria-hidden="true"></span>
<span class="pp-15__bars" aria-hidden="true"><i></i><i></i></span>
</label>
<p class="pp-15__hint">PRESS TO START</p>
</div>
<p class="pp-15__chip" aria-hidden="true">travel + matching shadow shrink · stepped clip-path pixel art · 100ms linear</p>
</div>
</section>.pp-15,
.pp-15 *,
.pp-15 *::before,
.pp-15 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.pp-15 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--pp-15-bg);
--pp-15-bg: oklch(0.2 0.03 285);
--pp-15-shell: oklch(0.27 0.04 285);
--pp-15-ink: oklch(0.96 0.02 100);
--pp-15-mut: oklch(0.72 0.04 285);
--pp-15-cap: oklch(0.68 0.2 25);
--pp-15-plinth: oklch(0.44 0.16 25);
--pp-15-crt: oklch(0.85 0.2 145);
font-family: ui-monospace,'SFMono-Regular',Menlo,Consolas,monospace;
color: var(--pp-15-ink);
}
.pp-15__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: 24px;
padding: clamp(20px,5vw,56px);
background: radial-gradient(70% 55% at 50% 12%,oklch(0.3 0.07 300/.7),transparent 72%);
}
.pp-15__cabinet {
display: grid;
justify-items: center;
gap: 18px;
width: min(100%,380px);
padding: 26px 28px 30px;
border: 4px solid oklch(0.16 0.03 285);
border-radius: 20px;
background: var(--pp-15-shell);
box-shadow: inset 0 2px 0 oklch(1 0 0/.12),0 26px 60px oklch(0 0 0/.55);
}
.pp-15__marquee {
font-size: 11px;
letter-spacing: .22em;
color: var(--pp-15-mut);
}
.pp-15__screen {
position: relative;
display: grid;
place-items: center;
width: 100%;
height: 96px;
border-radius: 8px;
background: oklch(0.14 0.04 160);
box-shadow: inset 0 0 24px oklch(0.3 0.14 150/.35),inset 0 0 0 2px oklch(0 0 0/.5);
overflow: hidden;
}
.pp-15__scan {
position: absolute;
inset: 0;
background: repeating-linear-gradient(to bottom,oklch(0 0 0/.35) 0 1px,transparent 1px 3px);
}
.pp-15__ready,
.pp-15__go {
font-size: 22px;
letter-spacing: .16em;
color: var(--pp-15-crt);
text-shadow: 0 0 12px currentColor;
}
.pp-15__go {
display: none;
animation: pp-15-blink 1s steps(2,end) infinite;
}
.pp-15-chk {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
clip-path: inset(50%);
}
.pp-15__cap {
display: grid;
place-items: center;
width: 104px;
height: 104px;
margin-top: 6px;
border-radius: 22px;
background: linear-gradient(180deg,color-mix(in oklch,var(--pp-15-cap) 88%,white),var(--pp-15-cap));
cursor: pointer;
box-shadow: 0 8px 0 var(--pp-15-plinth),0 10px 18px oklch(0 0 0/.45),inset 0 2px 0 oklch(1 0 0/.35);
transition: translate .1s linear,box-shadow .1s linear;
}
.pp-15__cap:active {
translate: 0 6px;
box-shadow: 0 2px 0 var(--pp-15-plinth),0 3px 8px oklch(0 0 0/.45),inset 0 2px 0 oklch(1 0 0/.35);
}
.pp-15-chk:focus-visible+.pp-15__cap {
outline: 3px dashed var(--pp-15-crt);
outline-offset: 6px;
}
.pp-15__px {
width: 46px;
height: 46px;
background: oklch(0.16 0.03 25);
clip-path: polygon(0 0,25% 12.5%,25% 25%,50% 25%,50% 37.5%,75% 37.5%,75% 62.5%,50% 62.5%,50% 75%,25% 75%,25% 87.5%,0 100%);
translate: 4px 0;
}
.pp-15__bars {
display: none;
gap: 10px;
width: 44px;
height: 46px;
}
.pp-15__bars i {
flex: 1;
background: oklch(0.16 0.03 25);
}
.pp-15-chk:checked+.pp-15__cap {
translate: 0 6px;
box-shadow: 0 2px 0 var(--pp-15-plinth),0 3px 8px oklch(0 0 0/.45),inset 0 2px 0 oklch(1 0 0/.35);
}
.pp-15-chk:checked+.pp-15__cap .pp-15__px {
display: none;
}
.pp-15-chk:checked+.pp-15__cap .pp-15__bars {
display: flex;
}
.pp-15__cabinet:has(.pp-15-chk:checked) .pp-15__ready {
display: none;
}
.pp-15__cabinet:has(.pp-15-chk:checked) .pp-15__go {
display: block;
}
@keyframes pp-15-blink {
0%,
50% {
opacity: 1;
}
51%,
100% {
opacity: .25;
}
}
.pp-15__hint {
font-size: 10.5px;
letter-spacing: .24em;
color: var(--pp-15-mut);
}
.pp-15__chip {
font-size: 11.5px;
color: var(--pp-15-mut);
padding: 7px 14px;
border: 1px solid oklch(1 0 0/.14);
border-radius: 99px;
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.pp-15__go {
animation: none;
}
.pp-15 * {
transition-duration: .01ms !important;
}
}.pp-15,
.pp-15 *,
.pp-15 *::before,
.pp-15 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.pp-15 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--pp-15-bg);
--pp-15-bg: oklch(0.2 0.03 285);
--pp-15-shell: oklch(0.27 0.04 285);
--pp-15-ink: oklch(0.96 0.02 100);
--pp-15-mut: oklch(0.72 0.04 285);
--pp-15-cap: oklch(0.68 0.2 25);
--pp-15-plinth: oklch(0.44 0.16 25);
--pp-15-crt: oklch(0.85 0.2 145);
font-family: ui-monospace,'SFMono-Regular',Menlo,Consolas,monospace;
color: var(--pp-15-ink);
}
.pp-15__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: 24px;
padding: clamp(20px,5vw,56px);
background: radial-gradient(70% 55% at 50% 12%,oklch(0.3 0.07 300/.7),transparent 72%);
}
.pp-15__cabinet {
display: grid;
justify-items: center;
gap: 18px;
width: min(100%,380px);
padding: 26px 28px 30px;
border: 4px solid oklch(0.16 0.03 285);
border-radius: 20px;
background: var(--pp-15-shell);
box-shadow: inset 0 2px 0 oklch(1 0 0/.12),0 26px 60px oklch(0 0 0/.55);
}
.pp-15__marquee {
font-size: 11px;
letter-spacing: .22em;
color: var(--pp-15-mut);
}
.pp-15__screen {
position: relative;
display: grid;
place-items: center;
width: 100%;
height: 96px;
border-radius: 8px;
background: oklch(0.14 0.04 160);
box-shadow: inset 0 0 24px oklch(0.3 0.14 150/.35),inset 0 0 0 2px oklch(0 0 0/.5);
overflow: hidden;
}
.pp-15__scan {
position: absolute;
inset: 0;
background: repeating-linear-gradient(to bottom,oklch(0 0 0/.35) 0 1px,transparent 1px 3px);
}
.pp-15__ready,
.pp-15__go {
font-size: 22px;
letter-spacing: .16em;
color: var(--pp-15-crt);
text-shadow: 0 0 12px currentColor;
}
.pp-15__go {
display: none;
animation: pp-15-blink 1s steps(2,end) infinite;
}
.pp-15-chk {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
clip-path: inset(50%);
}
.pp-15__cap {
display: grid;
place-items: center;
width: 104px;
height: 104px;
margin-top: 6px;
border-radius: 22px;
background: linear-gradient(180deg,color-mix(in oklch,var(--pp-15-cap) 88%,white),var(--pp-15-cap));
cursor: pointer;
box-shadow: 0 8px 0 var(--pp-15-plinth),0 10px 18px oklch(0 0 0/.45),inset 0 2px 0 oklch(1 0 0/.35);
transition: translate .1s linear,box-shadow .1s linear;
}
.pp-15__cap:active {
translate: 0 6px;
box-shadow: 0 2px 0 var(--pp-15-plinth),0 3px 8px oklch(0 0 0/.45),inset 0 2px 0 oklch(1 0 0/.35);
}
.pp-15-chk:focus-visible+.pp-15__cap {
outline: 3px dashed var(--pp-15-crt);
outline-offset: 6px;
}
.pp-15__px {
width: 46px;
height: 46px;
background: oklch(0.16 0.03 25);
clip-path: polygon(0 0,25% 12.5%,25% 25%,50% 25%,50% 37.5%,75% 37.5%,75% 62.5%,50% 62.5%,50% 75%,25% 75%,25% 87.5%,0 100%);
translate: 4px 0;
}
.pp-15__bars {
display: none;
gap: 10px;
width: 44px;
height: 46px;
}
.pp-15__bars i {
flex: 1;
background: oklch(0.16 0.03 25);
}
.pp-15-chk:checked+.pp-15__cap {
translate: 0 6px;
box-shadow: 0 2px 0 var(--pp-15-plinth),0 3px 8px oklch(0 0 0/.45),inset 0 2px 0 oklch(1 0 0/.35);
}
.pp-15-chk:checked+.pp-15__cap .pp-15__px {
display: none;
}
.pp-15-chk:checked+.pp-15__cap .pp-15__bars {
display: flex;
}
.pp-15__cabinet:has(.pp-15-chk:checked) .pp-15__ready {
display: none;
}
.pp-15__cabinet:has(.pp-15-chk:checked) .pp-15__go {
display: block;
}
@keyframes pp-15-blink {
0%,
50% {
opacity: 1;
}
51%,
100% {
opacity: .25;
}
}
.pp-15__hint {
font-size: 10.5px;
letter-spacing: .24em;
color: var(--pp-15-mut);
}
.pp-15__chip {
font-size: 11.5px;
color: var(--pp-15-mut);
padding: 7px 14px;
border: 1px solid oklch(1 0 0/.14);
border-radius: 99px;
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.pp-15__go {
animation: none;
}
.pp-15 * {
transition-duration: .01ms !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: Retro Arcade Play Pause Button CSS
Source: https://codefronts.com/components/css-play-pause-buttons/neon-arcade/
A physical arcade key: a chunky cap sitting on a coloured plinth, with the whole thing travelling down into the plinth when pressed — the shadow shrinks by exactly the distance the cap moves, which is what makes the depth read as real. The icon is genuine pixel art drawn with a stepped clip-path polygon, so it stays crisp at any size without a single image.
## HTML
```html
<section class="pp-15" aria-label="Retro arcade play pause button demo">
<div class="pp-15__stage">
<div class="pp-15__cabinet">
<p class="pp-15__marquee">INSERT COIN — SIDE B</p>
<div class="pp-15__screen" aria-hidden="true">
<span class="pp-15__scan"></span>
<p class="pp-15__ready">READY</p>
<p class="pp-15__go">PLAYING</p>
</div>
<input class="pp-15-chk" type="checkbox" id="pp-15-toggle" aria-label="Play or pause">
<label class="pp-15__cap" for="pp-15-toggle">
<span class="pp-15__px" aria-hidden="true"></span>
<span class="pp-15__bars" aria-hidden="true"><i></i><i></i></span>
</label>
<p class="pp-15__hint">PRESS TO START</p>
</div>
<p class="pp-15__chip" aria-hidden="true">travel + matching shadow shrink · stepped clip-path pixel art · 100ms linear</p>
</div>
</section>
```
## CSS
```css
.pp-15,
.pp-15 *,
.pp-15 *::before,
.pp-15 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.pp-15 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--pp-15-bg);
--pp-15-bg: oklch(0.2 0.03 285);
--pp-15-shell: oklch(0.27 0.04 285);
--pp-15-ink: oklch(0.96 0.02 100);
--pp-15-mut: oklch(0.72 0.04 285);
--pp-15-cap: oklch(0.68 0.2 25);
--pp-15-plinth: oklch(0.44 0.16 25);
--pp-15-crt: oklch(0.85 0.2 145);
font-family: ui-monospace,'SFMono-Regular',Menlo,Consolas,monospace;
color: var(--pp-15-ink);
}
.pp-15__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: 24px;
padding: clamp(20px,5vw,56px);
background: radial-gradient(70% 55% at 50% 12%,oklch(0.3 0.07 300/.7),transparent 72%);
}
.pp-15__cabinet {
display: grid;
justify-items: center;
gap: 18px;
width: min(100%,380px);
padding: 26px 28px 30px;
border: 4px solid oklch(0.16 0.03 285);
border-radius: 20px;
background: var(--pp-15-shell);
box-shadow: inset 0 2px 0 oklch(1 0 0/.12),0 26px 60px oklch(0 0 0/.55);
}
.pp-15__marquee {
font-size: 11px;
letter-spacing: .22em;
color: var(--pp-15-mut);
}
.pp-15__screen {
position: relative;
display: grid;
place-items: center;
width: 100%;
height: 96px;
border-radius: 8px;
background: oklch(0.14 0.04 160);
box-shadow: inset 0 0 24px oklch(0.3 0.14 150/.35),inset 0 0 0 2px oklch(0 0 0/.5);
overflow: hidden;
}
.pp-15__scan {
position: absolute;
inset: 0;
background: repeating-linear-gradient(to bottom,oklch(0 0 0/.35) 0 1px,transparent 1px 3px);
}
.pp-15__ready,
.pp-15__go {
font-size: 22px;
letter-spacing: .16em;
color: var(--pp-15-crt);
text-shadow: 0 0 12px currentColor;
}
.pp-15__go {
display: none;
animation: pp-15-blink 1s steps(2,end) infinite;
}
.pp-15-chk {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
clip-path: inset(50%);
}
.pp-15__cap {
display: grid;
place-items: center;
width: 104px;
height: 104px;
margin-top: 6px;
border-radius: 22px;
background: linear-gradient(180deg,color-mix(in oklch,var(--pp-15-cap) 88%,white),var(--pp-15-cap));
cursor: pointer;
box-shadow: 0 8px 0 var(--pp-15-plinth),0 10px 18px oklch(0 0 0/.45),inset 0 2px 0 oklch(1 0 0/.35);
transition: translate .1s linear,box-shadow .1s linear;
}
.pp-15__cap:active {
translate: 0 6px;
box-shadow: 0 2px 0 var(--pp-15-plinth),0 3px 8px oklch(0 0 0/.45),inset 0 2px 0 oklch(1 0 0/.35);
}
.pp-15-chk:focus-visible+.pp-15__cap {
outline: 3px dashed var(--pp-15-crt);
outline-offset: 6px;
}
.pp-15__px {
width: 46px;
height: 46px;
background: oklch(0.16 0.03 25);
clip-path: polygon(0 0,25% 12.5%,25% 25%,50% 25%,50% 37.5%,75% 37.5%,75% 62.5%,50% 62.5%,50% 75%,25% 75%,25% 87.5%,0 100%);
translate: 4px 0;
}
.pp-15__bars {
display: none;
gap: 10px;
width: 44px;
height: 46px;
}
.pp-15__bars i {
flex: 1;
background: oklch(0.16 0.03 25);
}
.pp-15-chk:checked+.pp-15__cap {
translate: 0 6px;
box-shadow: 0 2px 0 var(--pp-15-plinth),0 3px 8px oklch(0 0 0/.45),inset 0 2px 0 oklch(1 0 0/.35);
}
.pp-15-chk:checked+.pp-15__cap .pp-15__px {
display: none;
}
.pp-15-chk:checked+.pp-15__cap .pp-15__bars {
display: flex;
}
.pp-15__cabinet:has(.pp-15-chk:checked) .pp-15__ready {
display: none;
}
.pp-15__cabinet:has(.pp-15-chk:checked) .pp-15__go {
display: block;
}
@keyframes pp-15-blink {
0%,
50% {
opacity: 1;
}
51%,
100% {
opacity: .25;
}
}
.pp-15__hint {
font-size: 10.5px;
letter-spacing: .24em;
color: var(--pp-15-mut);
}
.pp-15__chip {
font-size: 11.5px;
color: var(--pp-15-mut);
padding: 7px 14px;
border: 1px solid oklch(1 0 0/.14);
border-radius: 99px;
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.pp-15__go {
animation: none;
}
.pp-15 * {
transition-duration: .01ms !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: Retro Arcade Play Pause Button CSS
Source: https://codefronts.com/components/css-play-pause-buttons/neon-arcade/
A physical arcade key: a chunky cap sitting on a coloured plinth, with the whole thing travelling down into the plinth when pressed — the shadow shrinks by exactly the distance the cap moves, which is what makes the depth read as real. The icon is genuine pixel art drawn with a stepped clip-path polygon, so it stays crisp at any size without a single image.
## HTML
```html
<section class="pp-15" aria-label="Retro arcade play pause button demo">
<div class="pp-15__stage">
<div class="pp-15__cabinet">
<p class="pp-15__marquee">INSERT COIN — SIDE B</p>
<div class="pp-15__screen" aria-hidden="true">
<span class="pp-15__scan"></span>
<p class="pp-15__ready">READY</p>
<p class="pp-15__go">PLAYING</p>
</div>
<input class="pp-15-chk" type="checkbox" id="pp-15-toggle" aria-label="Play or pause">
<label class="pp-15__cap" for="pp-15-toggle">
<span class="pp-15__px" aria-hidden="true"></span>
<span class="pp-15__bars" aria-hidden="true"><i></i><i></i></span>
</label>
<p class="pp-15__hint">PRESS TO START</p>
</div>
<p class="pp-15__chip" aria-hidden="true">travel + matching shadow shrink · stepped clip-path pixel art · 100ms linear</p>
</div>
</section>
```
## CSS
```css
.pp-15,
.pp-15 *,
.pp-15 *::before,
.pp-15 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.pp-15 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--pp-15-bg);
--pp-15-bg: oklch(0.2 0.03 285);
--pp-15-shell: oklch(0.27 0.04 285);
--pp-15-ink: oklch(0.96 0.02 100);
--pp-15-mut: oklch(0.72 0.04 285);
--pp-15-cap: oklch(0.68 0.2 25);
--pp-15-plinth: oklch(0.44 0.16 25);
--pp-15-crt: oklch(0.85 0.2 145);
font-family: ui-monospace,'SFMono-Regular',Menlo,Consolas,monospace;
color: var(--pp-15-ink);
}
.pp-15__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: 24px;
padding: clamp(20px,5vw,56px);
background: radial-gradient(70% 55% at 50% 12%,oklch(0.3 0.07 300/.7),transparent 72%);
}
.pp-15__cabinet {
display: grid;
justify-items: center;
gap: 18px;
width: min(100%,380px);
padding: 26px 28px 30px;
border: 4px solid oklch(0.16 0.03 285);
border-radius: 20px;
background: var(--pp-15-shell);
box-shadow: inset 0 2px 0 oklch(1 0 0/.12),0 26px 60px oklch(0 0 0/.55);
}
.pp-15__marquee {
font-size: 11px;
letter-spacing: .22em;
color: var(--pp-15-mut);
}
.pp-15__screen {
position: relative;
display: grid;
place-items: center;
width: 100%;
height: 96px;
border-radius: 8px;
background: oklch(0.14 0.04 160);
box-shadow: inset 0 0 24px oklch(0.3 0.14 150/.35),inset 0 0 0 2px oklch(0 0 0/.5);
overflow: hidden;
}
.pp-15__scan {
position: absolute;
inset: 0;
background: repeating-linear-gradient(to bottom,oklch(0 0 0/.35) 0 1px,transparent 1px 3px);
}
.pp-15__ready,
.pp-15__go {
font-size: 22px;
letter-spacing: .16em;
color: var(--pp-15-crt);
text-shadow: 0 0 12px currentColor;
}
.pp-15__go {
display: none;
animation: pp-15-blink 1s steps(2,end) infinite;
}
.pp-15-chk {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
clip-path: inset(50%);
}
.pp-15__cap {
display: grid;
place-items: center;
width: 104px;
height: 104px;
margin-top: 6px;
border-radius: 22px;
background: linear-gradient(180deg,color-mix(in oklch,var(--pp-15-cap) 88%,white),var(--pp-15-cap));
cursor: pointer;
box-shadow: 0 8px 0 var(--pp-15-plinth),0 10px 18px oklch(0 0 0/.45),inset 0 2px 0 oklch(1 0 0/.35);
transition: translate .1s linear,box-shadow .1s linear;
}
.pp-15__cap:active {
translate: 0 6px;
box-shadow: 0 2px 0 var(--pp-15-plinth),0 3px 8px oklch(0 0 0/.45),inset 0 2px 0 oklch(1 0 0/.35);
}
.pp-15-chk:focus-visible+.pp-15__cap {
outline: 3px dashed var(--pp-15-crt);
outline-offset: 6px;
}
.pp-15__px {
width: 46px;
height: 46px;
background: oklch(0.16 0.03 25);
clip-path: polygon(0 0,25% 12.5%,25% 25%,50% 25%,50% 37.5%,75% 37.5%,75% 62.5%,50% 62.5%,50% 75%,25% 75%,25% 87.5%,0 100%);
translate: 4px 0;
}
.pp-15__bars {
display: none;
gap: 10px;
width: 44px;
height: 46px;
}
.pp-15__bars i {
flex: 1;
background: oklch(0.16 0.03 25);
}
.pp-15-chk:checked+.pp-15__cap {
translate: 0 6px;
box-shadow: 0 2px 0 var(--pp-15-plinth),0 3px 8px oklch(0 0 0/.45),inset 0 2px 0 oklch(1 0 0/.35);
}
.pp-15-chk:checked+.pp-15__cap .pp-15__px {
display: none;
}
.pp-15-chk:checked+.pp-15__cap .pp-15__bars {
display: flex;
}
.pp-15__cabinet:has(.pp-15-chk:checked) .pp-15__ready {
display: none;
}
.pp-15__cabinet:has(.pp-15-chk:checked) .pp-15__go {
display: block;
}
@keyframes pp-15-blink {
0%,
50% {
opacity: 1;
}
51%,
100% {
opacity: .25;
}
}
.pp-15__hint {
font-size: 10.5px;
letter-spacing: .24em;
color: var(--pp-15-mut);
}
.pp-15__chip {
font-size: 11.5px;
color: var(--pp-15-mut);
padding: 7px 14px;
border: 1px solid oklch(1 0 0/.14);
border-radius: 99px;
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.pp-15__go {
animation: none;
}
.pp-15 * {
transition-duration: .01ms !important;
}
}
```How this works
3D press = one translate plus a shadow that shortens by the same amount. Keep them in lockstep and the eye reads solid geometry:
.cap{ box-shadow:0 8px 0 var(--plinth), 0 10px 16px rgb(0 0 0/.4);
transition:translate .1s, box-shadow .1s; }
.cap:active,
:checked + .cap{ translate:0 6px;
box-shadow:0 2px 0 var(--plinth), 0 3px 8px rgb(0 0 0/.4); }8px of shadow minus 6px of travel leaves 2px — the cap is now nearly flush with its base. Any other pairing (a shadow that stays put, or a scale instead of a translate) instantly looks like a flat rectangle changing colour. 100 ms is deliberate too: hardware keys have no ease-out.
The pixel triangle is a single stepped polygon — a staircase down the hypotenuse:
clip-path:polygon(0 0, 25% 12.5%, 25% 25%, 50% 25%, 50% 37.5%,
75% 37.5%, 75% 62.5%, 50% 62.5%, 50% 75%, 25% 75%, 25% 87.5%, 0 100%);Percentages mean it scales like vector art but keeps the chunky 8-bit staircase — the aesthetic of a bitmap with none of the resolution problems. The cabinet around it uses a repeating-linear-gradient for the CRT grille and a hard 4px inset border for the moulded plastic bezel.
Make it yours
- Key colour per action: --pp-15-cap and --pp-15-plinth as a pair; red for stop, yellow for pause, green for play — classic cabinet colour coding.
- Coin-op sound cue: pair the press with a 40 ms Web Audio square-wave blip if your app already has an audio context.
- Bigger travel: 10px shadow / 8px travel exaggerates the throw for a mechanical, keyboard-switch feel.
- Scanline CRT frame: increase the grille gradient opacity and add a subtle inset vignette to the cabinet for a heavier retro look.
Gotchas — read before shipping
- The shadow must shrink by the travel distance, or the button appears to slide rather than depress.
- Skip the ease: hardware travel is linear and fast. Anything over ~120 ms feels rubbery.
- Stepped clip-path polygons need matching point counts if you want to morph between them — see demo 08. Here we swap icons instead.
- Pixel fonts and tight letterspacing hurt legibility at small sizes; keep body copy in a normal UI font and reserve the retro type for the label.
- A chunky press effect can hide focus. This demo keeps a dashed outline offset outside the cap so it survives the travel.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 113+ | 111+ |
clip-path polygon, box-shadow and transitions are universal (Chrome 55+, Safari 9.1+, Firefox 54+). Only the oklch() tokens set the modern floor.