SolutionsCase StudiesAI ToolsResourcesAboutContact
Get Your Free AI Automation Audit Book a Free 30-Minute Strategy Call
Playground~/frontend Open editor

~/css/play-pause-buttons

7 CSS Play / Pause Buttons

3 Pure CSS4 with JSPublished 2026-10-05Free · no sign-up

Seven media controls: a play button that twists into pause, a podcast button with a progress ring, a frosted play button over a video poster, a mini audio player, a CSS-only toggle built on a checkbox, a live stream button with a pulsing halo and a track button with equaliser bars.

01. Play to Pause with a Twist

MediaToggle

One button toggles between a play triangle and two pause bars: both icons sit in the same grid cell and swap with a short rotate-and-fade. The aria-label switches between Play and Pause so the button always names what it will do next.

View code
<button class="pp1" aria-label="Play"><svg class="pl" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5l11 7-11 7z"/></svg><svg class="pa" viewBox="0 0 24 24" aria-hidden="true"><path d="M6.5 5h4v14h-4zM13.5 5h4v14h-4z"/></svg></button>
.pp1 { display: grid; place-items: center; width: 72px; height: 72px; border: 0; border-radius: 50%; background: #f8fafc; cursor: pointer; box-shadow: 0 10px 30px -10px rgba(0,0,0,.6); }
.pp1 svg { grid-area: 1 / 1; width: 30px; height: 30px; fill: #0f172a; transition: transform .3s ease, opacity .3s ease; }
.pp1 .pl { margin-left: 3px; }
.pp1 .pa { opacity: 0; transform: rotate(-90deg) scale(.6); }
.pp1.on .pl { opacity: 0; transform: rotate(90deg) scale(.6); }
.pp1.on .pa { opacity: 1; transform: none; }
.pp1:focus-visible { outline: 3px solid #38bdf8; outline-offset: 4px; }
@media (prefers-reduced-motion: reduce) { .pp1 svg { transition: none; } }
var b = document.querySelector('.pp1');
b.addEventListener('click', function () {
  var on = b.classList.toggle('on');
  b.setAttribute('aria-label', on ? 'Pause' : 'Play');
});

02. Play Button with Progress Ring

MediaProgress

A podcast-style play button whose ring fills as the episode plays. The ring is an SVG circle whose stroke-dashoffset is set from the elapsed time, pausing stops the pretend playback timer, and the elapsed time is written out in text beside the button.

View code
<div class="pp2"><button aria-label="Play episode"><svg class="ring" viewBox="0 0 64 64" aria-hidden="true"><circle cx="32" cy="32" r="29"/><circle class="v" cx="32" cy="32" r="29" pathLength="100"/></svg><svg class="ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5l11 7-11 7z"/></svg></button><span class="time">0:00 / 0:30</span></div>
.pp2 { display: flex; align-items: center; gap: 14px; color: #cbd5f5; font: 600 14px "JetBrains Mono", ui-monospace, monospace; }
.pp2 button { position: relative; display: grid; place-items: center; width: 64px; height: 64px; padding: 0; border: 0; border-radius: 50%; background: #1b2238; cursor: pointer; }
.pp2 .ring { position: absolute; inset: 0; transform: rotate(-90deg); }
.pp2 circle { fill: none; stroke: #2e3757; stroke-width: 3; }
.pp2 .v { stroke: #f472b6; stroke-linecap: round; stroke-dasharray: 100; stroke-dashoffset: 100; transition: stroke-dashoffset 1s linear; }
.pp2 .ic { width: 24px; height: 24px; fill: #fff; }
.pp2 button:focus-visible { outline: 2px solid #f472b6; outline-offset: 3px; }
var b = document.querySelector('.pp2 button'), t = document.querySelector('.pp2 .time'), v = b.querySelector('.v'), ic = b.querySelector('.ic path'), s = 0, id = null;
function draw() { v.style.strokeDashoffset = 100 - s / 30 * 100; t.textContent = '0:' + String(s).padStart(2, '0') + ' / 0:30'; }
function stop() { clearInterval(id); id = null; ic.setAttribute('d', 'M8 5l11 7-11 7z'); b.setAttribute('aria-label', 'Play episode'); }
b.addEventListener('click', function () {
  if (id) { stop(); return; }
  if (s >= 30) { s = 0; draw(); }
  ic.setAttribute('d', 'M7 5h3.5v14H7zM13.5 5H17v14h-3.5z'); b.setAttribute('aria-label', 'Pause episode');
  id = setInterval(function () { s++; draw(); if (s >= 30) stop(); }, 1000);
});
draw();

03. Video Overlay Play Button

MediaVideo

A large translucent play button centred over a video poster, with a frosted backdrop so it reads on any image. The poster here is a CSS gradient stand-in. The button is a real button inside the figure, and its name says which video it plays.

View code
<figure class="pp3"><div class="poster" aria-hidden="true"></div><button aria-label="Play video: Glazing a bowl in 60 seconds"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5l11 7-11 7z"/></svg></button><figcaption>Glazing a bowl in 60 seconds</figcaption></figure>
.pp3 { position: relative; width: 300px; max-width: 100%; margin: 0; border-radius: 14px; overflow: hidden; font: 600 14px Inter, system-ui, sans-serif; }
.pp3 .poster { aspect-ratio: 16 / 9; background: radial-gradient(circle at 30% 40%, #fdba74, transparent 40%), linear-gradient(135deg, #7c2d12, #1e293b); }
.pp3 button { position: absolute; left: 50%; top: 42%; transform: translate(-50%, -50%); display: grid; place-items: center; width: 64px; height: 64px; border: 1px solid rgba(255,255,255,.5); border-radius: 50%; background: rgba(15,23,42,.35); -webkit-backdrop-filter: blur(8px); backdrop-filter: blur(8px); cursor: pointer; transition: transform .2s, background .2s; }
.pp3 button:hover { transform: translate(-50%, -50%) scale(1.08); background: rgba(15,23,42,.5); }
.pp3 svg { width: 26px; height: 26px; margin-left: 3px; fill: #fff; }
.pp3 figcaption { padding: 10px 12px; background: #fff; color: #1f2937; }
.pp3 button:focus-visible { outline: 3px solid #fff; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .pp3 button { transition: none; } }

04. Mini Audio Player Bar

MediaPlayer

A compact player with previous, play or pause and next buttons, a track title and a progress bar. Each icon button has an aria-label, the play button toggles aria-pressed, and progress is a native progress element with its own label.

View code
<div class="pp4"><div class="meta"><b>Morning Ferry</b><span>Lumen Quartet</span></div><div class="ctl"><button aria-label="Previous track"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 5h2v14H6zM19 5L9 12l10 7z"/></svg></button><button class="play" aria-label="Play" aria-pressed="false"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5l11 7-11 7z"/></svg></button><button aria-label="Next track"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M16 5h2v14h-2zM5 5l10 7-10 7z"/></svg></button></div><progress value="38" max="100" aria-label="Track progress">38%</progress></div>
.pp4 { box-sizing: border-box; width: 300px; max-width: 100%; padding: 16px; border: 1px solid #e4e7ec; border-radius: 16px; background: #fff; font: 14px Inter, system-ui, sans-serif; color: #101828; }
.pp4 .meta { display: grid; margin-bottom: 10px; } .pp4 .meta span { color: #475467; font-size: 13px; }
.pp4 .ctl { display: flex; justify-content: center; align-items: center; gap: 14px; }
.pp4 button { display: grid; place-items: center; width: 40px; height: 40px; border: 0; border-radius: 50%; background: transparent; cursor: pointer; }
.pp4 button svg { width: 20px; height: 20px; fill: #344054; }
.pp4 .play { width: 52px; height: 52px; background: #101828; }
.pp4 .play svg { fill: #fff; }
.pp4 button:hover:not(.play) { background: #f2f4f7; }
.pp4 progress { width: 100%; height: 6px; margin-top: 12px; appearance: none; border: 0; border-radius: 3px; background: #eaecf0; overflow: hidden; }
.pp4 progress::-webkit-progress-bar { background: #eaecf0; }
.pp4 progress::-webkit-progress-value { background: #101828; }
.pp4 progress::-moz-progress-bar { background: #101828; }
.pp4 button:focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }
var p = document.querySelector('.pp4 .play'), path = p.querySelector('path');
p.addEventListener('click', function () {
  var on = p.getAttribute('aria-pressed') !== 'true';
  p.setAttribute('aria-pressed', on); p.setAttribute('aria-label', on ? 'Pause' : 'Play');
  path.setAttribute('d', on ? 'M7 5h3.5v14H7zM13.5 5H17v14h-3.5z' : 'M8 5l11 7-11 7z');
});

05. CSS-Only Play Toggle

MediaNo JS

A play and pause toggle with no JavaScript: a visually hidden checkbox inside a label, with the icons switched by :checked. The checkbox is named Playing, so screen readers announce a checkbox that is checked or not checked, and :has() draws the focus ring.

View code
<label class="pp5"><input type="checkbox"><span class="sr">Playing</span><svg class="pl" viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5l11 7-11 7z"/></svg><svg class="pa" viewBox="0 0 24 24" aria-hidden="true"><path d="M7 5h3.5v14H7zM13.5 5H17v14h-3.5z"/></svg></label>
.pp5 { position: relative; display: grid; place-items: center; width: 64px; height: 64px; border-radius: 18px; background: linear-gradient(145deg, #6d28d9, #4338ca); cursor: pointer; box-shadow: 0 10px 24px -10px #6d28d9; }
.pp5 input, .pp5 .sr { position: absolute; width: 1px; height: 1px; opacity: 0; overflow: hidden; }
.pp5 svg { grid-area: 1 / 1; width: 26px; height: 26px; fill: #fff; transition: transform .25s, opacity .25s; }
.pp5 .pa { opacity: 0; transform: scale(.5); }
.pp5 input:checked ~ .pl { opacity: 0; transform: scale(.5); }
.pp5 input:checked ~ .pa { opacity: 1; transform: none; }
.pp5:has(input:focus-visible) { outline: 3px solid #c4b5fd; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .pp5 svg { transition: none; } }

06. Live Stream Play Button

MediaLive

A play button with a LIVE label and a soft pulsing halo that tells people the stream is happening now. The halo is a scaled pseudo-element that fades as it grows, and the visible text Play live stream is the accessible name.

View code
<button class="pp6"><span class="c"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M8 5l11 7-11 7z"/></svg></span><span class="t">Play live stream</span><span class="live" aria-hidden="true">LIVE</span></button>
.pp6 { display: inline-flex; align-items: center; gap: 12px; padding: 8px 16px 8px 8px; border: 1px solid #3b2333; border-radius: 999px; background: #1b1220; color: #fde7ef; font: 700 15px Inter, system-ui, sans-serif; cursor: pointer; }
.pp6 .c { position: relative; isolation: isolate; display: grid; place-items: center; width: 44px; height: 44px; border-radius: 50%; background: #e11d48; }
.pp6 .c::before { content: ""; position: absolute; inset: 0; border-radius: 50%; background: #e11d48; animation: pp6 1.8s ease-out infinite; z-index: -1; }
.pp6 svg { width: 20px; height: 20px; margin-left: 2px; fill: #fff; }
.pp6 .live { padding: 2px 6px; border-radius: 4px; background: #be123c; color: #fff; font-size: 11px; letter-spacing: .08em; }
@keyframes pp6 { from { transform: scale(1); opacity: .6; } to { transform: scale(1.7); opacity: 0; } }
.pp6:focus-visible { outline: 2px solid #fda4af; outline-offset: 3px; }
@media (prefers-reduced-motion: reduce) { .pp6 .c::before { animation: none; } }

07. Now Playing Equaliser Button

MediaAnimation

While playing, the icon becomes three moving equaliser bars, a familiar now-playing signal in track lists. Pausing freezes the bars with animation-play-state rather than removing them, and the button text names the track so a list of these buttons stays distinct.

View code
<button class="pp7" aria-pressed="false"><span class="ic" aria-hidden="true"><i></i><i></i><i></i></span><span>Play “Harbour Lights”</span></button>
.pp7 { display: inline-flex; align-items: center; gap: 12px; padding: 12px 18px; border: 1px solid #d0d5dd; border-radius: 12px; background: #fff; color: #101828; font: 600 15px Inter, system-ui, sans-serif; cursor: pointer; }
.pp7 .ic { display: flex; align-items: flex-end; gap: 3px; width: 18px; height: 18px; }
.pp7 i { flex: 1; height: 30%; border-radius: 1px; background: #0e7c70; animation: pp7 .9s ease-in-out infinite alternate; animation-play-state: paused; }
.pp7 i:nth-child(2) { animation-delay: -.3s; height: 70%; } .pp7 i:nth-child(3) { animation-delay: -.6s; height: 50%; }
.pp7[aria-pressed="true"] i { animation-play-state: running; }
.pp7[aria-pressed="true"] { border-color: #0e7c70; background: #f0fdf9; }
@keyframes pp7 { from { height: 20%; } to { height: 100%; } }
.pp7:focus-visible { outline: 3px solid #5fe9d0; outline-offset: 2px; }
@media (prefers-reduced-motion: reduce) { .pp7 i { animation: none; } }
var b = document.querySelector('.pp7');
b.addEventListener('click', function () { b.setAttribute('aria-pressed', b.getAttribute('aria-pressed') === 'true' ? 'false' : 'true'); });

About these CSS Play / Pause Buttons

Name the next action

An icon-only media button needs an accessible name, and the name should change with the state: Play when paused, Pause when playing. Alternatively keep one name and toggle aria-pressed.

Icons as SVG

Simple SVG paths keep the icons sharp at any size and colourable with fill: currentColor or a set fill. Two icons stacked in the same grid cell can cross-fade without either one shifting the layout.

Big enough to hit

Play buttons are often the main control on a card or video, so make them large: at least 44 by 44 pixels, and bigger over video, where the poster image gives plenty of room.

Wire them to real media

Connect the button to an <audio> or <video> element by calling play() and pause(), and update the icon from the element's play and pause events so it stays in sync with the keyboard and system controls.

Frequently asked questions

Do these buttons play audio?

No. They show the control states with pretend timers; connect them to your own media element.

Can I make the toggle without JavaScript?

Yes. The CSS-only demo uses a hidden checkbox and :checked, but real playback still needs script.

Why aria-pressed on some and aria-label on others?

Both work. aria-pressed keeps one name and reports on or off; changing the label says the next action.