25 CSS Play / Pause Buttons13 / 25
Pulsing Halo Video Play Icon
The attention magnet used on every hero video: concentric rings that expand and fade out of the play button forever, drawing the eye without moving the layout. Two pseudo-elements and one keyframe produce an infinite radar pulse, staggered by a negative delay so a new ring leaves before the last one dies — and the pulse politely stops once the video is playing, because a control that keeps shouting after you've obeyed it is just noise.
Published Updated
The code
<section class="pp-13" aria-label="Pulsing halo video play icon demo">
<div class="pp-13__stage">
<div class="pp-13__thumb">
<img class="pp-13__img" src="https://images.unsplash.com/photo-1536440136628-849c177e76a1?q=80&w=1100&auto=format&fit=crop" alt="" loading="lazy">
<span class="pp-13__scrim" aria-hidden="true"></span>
<input class="pp-13-chk" type="checkbox" id="pp-13-toggle" aria-label="Play or pause the trailer">
<label class="pp-13__btn" for="pp-13-toggle">
<svg viewBox="0 0 24 24" width="30" height="30" aria-hidden="true">
<path class="pp-13__p" d="M9 5.4v13.2L19.2 12z" fill="currentColor"/>
<path class="pp-13__q" d="M8.4 5.4h3.2v13.2H8.4zM12.8 5.4H16v13.2h-3.2z" fill="currentColor"/>
</svg>
</label>
<div class="pp-13__meta">
<p class="pp-13__kicker">Official trailer</p>
<p class="pp-13__title">The Long Descent</p>
</div>
</div>
<p class="pp-13__chip" aria-hidden="true">two pseudo-element rings · negative delay stagger · scale + opacity only</p>
</div>
</section><section class="pp-13" aria-label="Pulsing halo video play icon demo">
<div class="pp-13__stage">
<div class="pp-13__thumb">
<img class="pp-13__img" src="https://images.unsplash.com/photo-1536440136628-849c177e76a1?q=80&w=1100&auto=format&fit=crop" alt="" loading="lazy">
<span class="pp-13__scrim" aria-hidden="true"></span>
<input class="pp-13-chk" type="checkbox" id="pp-13-toggle" aria-label="Play or pause the trailer">
<label class="pp-13__btn" for="pp-13-toggle">
<svg viewBox="0 0 24 24" width="30" height="30" aria-hidden="true">
<path class="pp-13__p" d="M9 5.4v13.2L19.2 12z" fill="currentColor"/>
<path class="pp-13__q" d="M8.4 5.4h3.2v13.2H8.4zM12.8 5.4H16v13.2h-3.2z" fill="currentColor"/>
</svg>
</label>
<div class="pp-13__meta">
<p class="pp-13__kicker">Official trailer</p>
<p class="pp-13__title">The Long Descent</p>
</div>
</div>
<p class="pp-13__chip" aria-hidden="true">two pseudo-element rings · negative delay stagger · scale + opacity only</p>
</div>
</section>.pp-13,
.pp-13 *,
.pp-13 *::before,
.pp-13 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.pp-13 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--pp-13-bg);
--pp-13-bg: oklch(0.13 0.015 265);
--pp-13-ink: oklch(0.98 0.004 250);
--pp-13-mut: oklch(0.72 0.02 255);
--pp-13-halo: oklch(0.95 0.02 250);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--pp-13-ink);
}
.pp-13__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: 22px;
padding: clamp(18px,4vw,52px);
}
.pp-13__thumb {
position: relative;
width: min(100%,720px);
aspect-ratio: 16/9;
border-radius: 20px;
overflow: hidden;
background: linear-gradient(140deg,oklch(0.35 0.06 250),oklch(0.18 0.03 280));
box-shadow: 0 30px 70px oklch(0 0 0/.55);
}
.pp-13__img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.pp-13__scrim {
position: absolute;
inset: 0;
background: radial-gradient(45% 45% at 50% 50%,oklch(0 0 0/.42),transparent 70%),linear-gradient(to top,oklch(0 0 0/.7),transparent 55%);
}
.pp-13-chk {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
clip-path: inset(50%);
}
.pp-13__btn {
position: absolute;
top: 50%;
left: 50%;
translate: -50% -50%;
display: grid;
place-items: center;
width: 92px;
height: 92px;
border-radius: 50%;
background: oklch(1 0 0/.16);
border: 2px solid oklch(1 0 0/.55);
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
color: var(--pp-13-ink);
cursor: pointer;
transition: background .3s,scale .3s cubic-bezier(.34,1.4,.5,1),opacity .3s;
}
.pp-13__btn::before,
.pp-13__btn::after {
content: '';
position: absolute;
inset: -2px;
border-radius: 50%;
border: 2px solid var(--pp-13-halo);
pointer-events: none;
animation: pp-13-pulse 2.6s cubic-bezier(.25,.6,.3,1) infinite;
}
.pp-13__btn::after {
animation-delay: -1.3s;
}
.pp-13__btn:hover {
background: oklch(1 0 0/.28);
scale: 1.06;
}
.pp-13-chk:focus-visible+.pp-13__btn {
outline: 3px solid var(--pp-13-halo);
outline-offset: 6px;
}
@keyframes pp-13-pulse {
from {
scale: 1;
opacity: .8;
}
to {
scale: 2.4;
opacity: 0;
}
}
.pp-13__q {
display: none;
}
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__btn::before,
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__btn::after {
animation: none;
opacity: 0;
}
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__btn {
scale: .82;
opacity: .35;
}
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__btn:hover,
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__btn:focus-visible {
opacity: 1;
scale: .9;
}
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__p {
display: none;
}
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__q {
display: block;
}
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__scrim {
opacity: .45;
}
.pp-13__meta {
position: absolute;
left: 26px;
bottom: 24px;
}
.pp-13__kicker {
font-size: 11px;
letter-spacing: .2em;
text-transform: uppercase;
color: var(--pp-13-mut);
}
.pp-13__title {
font-size: clamp(20px,3vw,28px);
font-weight: 670;
letter-spacing: -.02em;
margin-top: 6px;
text-shadow: 0 2px 14px oklch(0 0 0/.6);
}
.pp-13__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--pp-13-mut);
padding: 7px 14px;
border: 1px solid oklch(1 0 0/.13);
border-radius: 99px;
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.pp-13__btn::before,
.pp-13__btn::after {
animation: none;
opacity: 0;
}
.pp-13 * {
transition-duration: .01ms !important;
}
}.pp-13,
.pp-13 *,
.pp-13 *::before,
.pp-13 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.pp-13 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--pp-13-bg);
--pp-13-bg: oklch(0.13 0.015 265);
--pp-13-ink: oklch(0.98 0.004 250);
--pp-13-mut: oklch(0.72 0.02 255);
--pp-13-halo: oklch(0.95 0.02 250);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--pp-13-ink);
}
.pp-13__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: 22px;
padding: clamp(18px,4vw,52px);
}
.pp-13__thumb {
position: relative;
width: min(100%,720px);
aspect-ratio: 16/9;
border-radius: 20px;
overflow: hidden;
background: linear-gradient(140deg,oklch(0.35 0.06 250),oklch(0.18 0.03 280));
box-shadow: 0 30px 70px oklch(0 0 0/.55);
}
.pp-13__img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.pp-13__scrim {
position: absolute;
inset: 0;
background: radial-gradient(45% 45% at 50% 50%,oklch(0 0 0/.42),transparent 70%),linear-gradient(to top,oklch(0 0 0/.7),transparent 55%);
}
.pp-13-chk {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
clip-path: inset(50%);
}
.pp-13__btn {
position: absolute;
top: 50%;
left: 50%;
translate: -50% -50%;
display: grid;
place-items: center;
width: 92px;
height: 92px;
border-radius: 50%;
background: oklch(1 0 0/.16);
border: 2px solid oklch(1 0 0/.55);
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
color: var(--pp-13-ink);
cursor: pointer;
transition: background .3s,scale .3s cubic-bezier(.34,1.4,.5,1),opacity .3s;
}
.pp-13__btn::before,
.pp-13__btn::after {
content: '';
position: absolute;
inset: -2px;
border-radius: 50%;
border: 2px solid var(--pp-13-halo);
pointer-events: none;
animation: pp-13-pulse 2.6s cubic-bezier(.25,.6,.3,1) infinite;
}
.pp-13__btn::after {
animation-delay: -1.3s;
}
.pp-13__btn:hover {
background: oklch(1 0 0/.28);
scale: 1.06;
}
.pp-13-chk:focus-visible+.pp-13__btn {
outline: 3px solid var(--pp-13-halo);
outline-offset: 6px;
}
@keyframes pp-13-pulse {
from {
scale: 1;
opacity: .8;
}
to {
scale: 2.4;
opacity: 0;
}
}
.pp-13__q {
display: none;
}
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__btn::before,
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__btn::after {
animation: none;
opacity: 0;
}
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__btn {
scale: .82;
opacity: .35;
}
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__btn:hover,
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__btn:focus-visible {
opacity: 1;
scale: .9;
}
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__p {
display: none;
}
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__q {
display: block;
}
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__scrim {
opacity: .45;
}
.pp-13__meta {
position: absolute;
left: 26px;
bottom: 24px;
}
.pp-13__kicker {
font-size: 11px;
letter-spacing: .2em;
text-transform: uppercase;
color: var(--pp-13-mut);
}
.pp-13__title {
font-size: clamp(20px,3vw,28px);
font-weight: 670;
letter-spacing: -.02em;
margin-top: 6px;
text-shadow: 0 2px 14px oklch(0 0 0/.6);
}
.pp-13__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--pp-13-mut);
padding: 7px 14px;
border: 1px solid oklch(1 0 0/.13);
border-radius: 99px;
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.pp-13__btn::before,
.pp-13__btn::after {
animation: none;
opacity: 0;
}
.pp-13 * {
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: Pulsing Halo Video Play Icon
Source: https://codefronts.com/components/css-play-pause-buttons/pulse-halo/
The attention magnet used on every hero video: concentric rings that expand and fade out of the play button forever, drawing the eye without moving the layout. Two pseudo-elements and one keyframe produce an infinite radar pulse, staggered by a negative delay so a new ring leaves before the last one dies — and the pulse politely stops once the video is playing, because a control that keeps shouting after you've obeyed it is just noise.
## HTML
```html
<section class="pp-13" aria-label="Pulsing halo video play icon demo">
<div class="pp-13__stage">
<div class="pp-13__thumb">
<img class="pp-13__img" src="https://images.unsplash.com/photo-1536440136628-849c177e76a1?q=80&w=1100&auto=format&fit=crop" alt="" loading="lazy">
<span class="pp-13__scrim" aria-hidden="true"></span>
<input class="pp-13-chk" type="checkbox" id="pp-13-toggle" aria-label="Play or pause the trailer">
<label class="pp-13__btn" for="pp-13-toggle">
<svg viewBox="0 0 24 24" width="30" height="30" aria-hidden="true">
<path class="pp-13__p" d="M9 5.4v13.2L19.2 12z" fill="currentColor"/>
<path class="pp-13__q" d="M8.4 5.4h3.2v13.2H8.4zM12.8 5.4H16v13.2h-3.2z" fill="currentColor"/>
</svg>
</label>
<div class="pp-13__meta">
<p class="pp-13__kicker">Official trailer</p>
<p class="pp-13__title">The Long Descent</p>
</div>
</div>
<p class="pp-13__chip" aria-hidden="true">two pseudo-element rings · negative delay stagger · scale + opacity only</p>
</div>
</section>
```
## CSS
```css
.pp-13,
.pp-13 *,
.pp-13 *::before,
.pp-13 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.pp-13 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--pp-13-bg);
--pp-13-bg: oklch(0.13 0.015 265);
--pp-13-ink: oklch(0.98 0.004 250);
--pp-13-mut: oklch(0.72 0.02 255);
--pp-13-halo: oklch(0.95 0.02 250);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--pp-13-ink);
}
.pp-13__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: 22px;
padding: clamp(18px,4vw,52px);
}
.pp-13__thumb {
position: relative;
width: min(100%,720px);
aspect-ratio: 16/9;
border-radius: 20px;
overflow: hidden;
background: linear-gradient(140deg,oklch(0.35 0.06 250),oklch(0.18 0.03 280));
box-shadow: 0 30px 70px oklch(0 0 0/.55);
}
.pp-13__img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.pp-13__scrim {
position: absolute;
inset: 0;
background: radial-gradient(45% 45% at 50% 50%,oklch(0 0 0/.42),transparent 70%),linear-gradient(to top,oklch(0 0 0/.7),transparent 55%);
}
.pp-13-chk {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
clip-path: inset(50%);
}
.pp-13__btn {
position: absolute;
top: 50%;
left: 50%;
translate: -50% -50%;
display: grid;
place-items: center;
width: 92px;
height: 92px;
border-radius: 50%;
background: oklch(1 0 0/.16);
border: 2px solid oklch(1 0 0/.55);
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
color: var(--pp-13-ink);
cursor: pointer;
transition: background .3s,scale .3s cubic-bezier(.34,1.4,.5,1),opacity .3s;
}
.pp-13__btn::before,
.pp-13__btn::after {
content: '';
position: absolute;
inset: -2px;
border-radius: 50%;
border: 2px solid var(--pp-13-halo);
pointer-events: none;
animation: pp-13-pulse 2.6s cubic-bezier(.25,.6,.3,1) infinite;
}
.pp-13__btn::after {
animation-delay: -1.3s;
}
.pp-13__btn:hover {
background: oklch(1 0 0/.28);
scale: 1.06;
}
.pp-13-chk:focus-visible+.pp-13__btn {
outline: 3px solid var(--pp-13-halo);
outline-offset: 6px;
}
@keyframes pp-13-pulse {
from {
scale: 1;
opacity: .8;
}
to {
scale: 2.4;
opacity: 0;
}
}
.pp-13__q {
display: none;
}
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__btn::before,
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__btn::after {
animation: none;
opacity: 0;
}
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__btn {
scale: .82;
opacity: .35;
}
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__btn:hover,
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__btn:focus-visible {
opacity: 1;
scale: .9;
}
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__p {
display: none;
}
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__q {
display: block;
}
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__scrim {
opacity: .45;
}
.pp-13__meta {
position: absolute;
left: 26px;
bottom: 24px;
}
.pp-13__kicker {
font-size: 11px;
letter-spacing: .2em;
text-transform: uppercase;
color: var(--pp-13-mut);
}
.pp-13__title {
font-size: clamp(20px,3vw,28px);
font-weight: 670;
letter-spacing: -.02em;
margin-top: 6px;
text-shadow: 0 2px 14px oklch(0 0 0/.6);
}
.pp-13__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--pp-13-mut);
padding: 7px 14px;
border: 1px solid oklch(1 0 0/.13);
border-radius: 99px;
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.pp-13__btn::before,
.pp-13__btn::after {
animation: none;
opacity: 0;
}
.pp-13 * {
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: Pulsing Halo Video Play Icon
Source: https://codefronts.com/components/css-play-pause-buttons/pulse-halo/
The attention magnet used on every hero video: concentric rings that expand and fade out of the play button forever, drawing the eye without moving the layout. Two pseudo-elements and one keyframe produce an infinite radar pulse, staggered by a negative delay so a new ring leaves before the last one dies — and the pulse politely stops once the video is playing, because a control that keeps shouting after you've obeyed it is just noise.
## HTML
```html
<section class="pp-13" aria-label="Pulsing halo video play icon demo">
<div class="pp-13__stage">
<div class="pp-13__thumb">
<img class="pp-13__img" src="https://images.unsplash.com/photo-1536440136628-849c177e76a1?q=80&w=1100&auto=format&fit=crop" alt="" loading="lazy">
<span class="pp-13__scrim" aria-hidden="true"></span>
<input class="pp-13-chk" type="checkbox" id="pp-13-toggle" aria-label="Play or pause the trailer">
<label class="pp-13__btn" for="pp-13-toggle">
<svg viewBox="0 0 24 24" width="30" height="30" aria-hidden="true">
<path class="pp-13__p" d="M9 5.4v13.2L19.2 12z" fill="currentColor"/>
<path class="pp-13__q" d="M8.4 5.4h3.2v13.2H8.4zM12.8 5.4H16v13.2h-3.2z" fill="currentColor"/>
</svg>
</label>
<div class="pp-13__meta">
<p class="pp-13__kicker">Official trailer</p>
<p class="pp-13__title">The Long Descent</p>
</div>
</div>
<p class="pp-13__chip" aria-hidden="true">two pseudo-element rings · negative delay stagger · scale + opacity only</p>
</div>
</section>
```
## CSS
```css
.pp-13,
.pp-13 *,
.pp-13 *::before,
.pp-13 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.pp-13 {
width: 100%;
min-height: 100vh;
min-height: 100svh;
display: block;
background: var(--pp-13-bg);
--pp-13-bg: oklch(0.13 0.015 265);
--pp-13-ink: oklch(0.98 0.004 250);
--pp-13-mut: oklch(0.72 0.02 255);
--pp-13-halo: oklch(0.95 0.02 250);
font-family: 'Segoe UI',system-ui,sans-serif;
color: var(--pp-13-ink);
}
.pp-13__stage {
min-height: 100vh;
min-height: 100svh;
display: grid;
place-items: center;
align-content: center;
gap: 22px;
padding: clamp(18px,4vw,52px);
}
.pp-13__thumb {
position: relative;
width: min(100%,720px);
aspect-ratio: 16/9;
border-radius: 20px;
overflow: hidden;
background: linear-gradient(140deg,oklch(0.35 0.06 250),oklch(0.18 0.03 280));
box-shadow: 0 30px 70px oklch(0 0 0/.55);
}
.pp-13__img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
.pp-13__scrim {
position: absolute;
inset: 0;
background: radial-gradient(45% 45% at 50% 50%,oklch(0 0 0/.42),transparent 70%),linear-gradient(to top,oklch(0 0 0/.7),transparent 55%);
}
.pp-13-chk {
position: absolute;
width: 1px;
height: 1px;
opacity: 0;
clip-path: inset(50%);
}
.pp-13__btn {
position: absolute;
top: 50%;
left: 50%;
translate: -50% -50%;
display: grid;
place-items: center;
width: 92px;
height: 92px;
border-radius: 50%;
background: oklch(1 0 0/.16);
border: 2px solid oklch(1 0 0/.55);
-webkit-backdrop-filter: blur(8px);
backdrop-filter: blur(8px);
color: var(--pp-13-ink);
cursor: pointer;
transition: background .3s,scale .3s cubic-bezier(.34,1.4,.5,1),opacity .3s;
}
.pp-13__btn::before,
.pp-13__btn::after {
content: '';
position: absolute;
inset: -2px;
border-radius: 50%;
border: 2px solid var(--pp-13-halo);
pointer-events: none;
animation: pp-13-pulse 2.6s cubic-bezier(.25,.6,.3,1) infinite;
}
.pp-13__btn::after {
animation-delay: -1.3s;
}
.pp-13__btn:hover {
background: oklch(1 0 0/.28);
scale: 1.06;
}
.pp-13-chk:focus-visible+.pp-13__btn {
outline: 3px solid var(--pp-13-halo);
outline-offset: 6px;
}
@keyframes pp-13-pulse {
from {
scale: 1;
opacity: .8;
}
to {
scale: 2.4;
opacity: 0;
}
}
.pp-13__q {
display: none;
}
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__btn::before,
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__btn::after {
animation: none;
opacity: 0;
}
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__btn {
scale: .82;
opacity: .35;
}
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__btn:hover,
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__btn:focus-visible {
opacity: 1;
scale: .9;
}
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__p {
display: none;
}
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__q {
display: block;
}
.pp-13__thumb:has(.pp-13-chk:checked) .pp-13__scrim {
opacity: .45;
}
.pp-13__meta {
position: absolute;
left: 26px;
bottom: 24px;
}
.pp-13__kicker {
font-size: 11px;
letter-spacing: .2em;
text-transform: uppercase;
color: var(--pp-13-mut);
}
.pp-13__title {
font-size: clamp(20px,3vw,28px);
font-weight: 670;
letter-spacing: -.02em;
margin-top: 6px;
text-shadow: 0 2px 14px oklch(0 0 0/.6);
}
.pp-13__chip {
font-family: ui-monospace,Menlo,Consolas,monospace;
font-size: 11.5px;
color: var(--pp-13-mut);
padding: 7px 14px;
border: 1px solid oklch(1 0 0/.13);
border-radius: 99px;
text-align: center;
}
@media (prefers-reduced-motion: reduce) {
.pp-13__btn::before,
.pp-13__btn::after {
animation: none;
opacity: 0;
}
.pp-13 * {
transition-duration: .01ms !important;
}
}
```How this works
Two rings, one keyframe, one offset — that is the entire effect:
.btn::before,.btn::after{
content:''; position:absolute; inset:0; border-radius:50%;
border:2px solid var(--halo);
animation:pp-13-pulse 2.6s cubic-bezier(.25,.6,.3,1) infinite; }
.btn::after{ animation-delay:-1.3s; } /* half a cycle later */
@keyframes pp-13-pulse{
from{ scale:1; opacity:.85 }
to { scale:2.4; opacity:0 } }Using scale + opacity (never width/height) keeps both rings on the compositor, so the pulse costs essentially nothing even behind a playing video. The easing matters more than the numbers: an ease-out curve makes the ring shoot outward and then drift, which is how real ripples behave; linear reads like a loading spinner.
The button lives inside a position:relative thumbnail with a gradient scrim, and the pulse is suppressed the moment playback starts via :has(:checked). The same rule also shrinks the button and drops its opacity, so the video is unobstructed while it plays and the control fades back in on hover.
Make it yours
- Three rings: add a third layer as a child span with delay -0.87s for a denser radar — beyond three it turns to mush.
- Filled sonar: swap the border for a background and start at opacity .35 for a softer, mist-like bloom.
- Brand pulse: --pp-13-halo can be any hue; on light backgrounds drop opacity to ~.5 so the ring doesn't overpower the image.
- Pulse only until first interaction: add the animation to a class you remove on click — attention-getting once, then quiet, which is the polite version of this pattern.
Gotchas — read before shipping
- Never animate width/height or box-shadow spread for pulses — both hit layout/paint every frame. scale + opacity only.
- Infinite motion next to text is a WCAG 2.2.2 concern: it must be stoppable. Here it stops on play; if yours never stops, provide a pause control.
- Pseudo-element rings can extend past overflow:hidden containers and get clipped — give the button room, or move the rings to a wrapper.
- pointer-events:none on the rings, or the outermost expanding ring will intercept clicks meant for the button.
- Don't stack the pulse on top of a poster with busy detail behind the button — add a subtle radial scrim first or the rings disappear into the image.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 113+ | 111+ |
Pseudo-element animation is universal. The independent scale property needs Chrome 104 / Safari 14.1 / Firefox 72; use transform:scale() for older engines. :has() gates the stop-on-play rule.