34 CSS Floating Buttons26 / 34

Pure CSSMIT licensed

Pulse Ring Attention-Getter Floating Button CSS

Three expanding rings on a stagger, the pattern used for call buttons, live indicators and onboarding spotlights. The important part is restraint: this demo shows the correct version (a short burst that stops), the radar variant, and a live-status dot — plus exactly why an infinite pulse on a persistent control is a mistake and when it is genuinely justified.

Published Updated

Live Demo
Try it

The code

<section class="fb-26" aria-label="Pulse ring attention getter button demo">
  <div class="fb-26__stage">
    <p class="fb-26__eyebrow">staggered rings · opacity leads scale</p>
    <h2 class="fb-26__title">Pulse with a reason</h2>
    <p class="fb-26__sub">Three rings, each a third of a cycle behind the last. Fade to zero before the scale ends, or the ring pops instead of dissolving.</p>
    <div class="fb-26__row">
      <div class="fb-26__cell">
        <span class="fb-26__wrap">
          <i class="fb-26__ring" aria-hidden="true"></i><i class="fb-26__ring" aria-hidden="true"></i><i class="fb-26__ring" aria-hidden="true"></i>
          <button class="fb-26__fab" type="button" aria-label="Start a call">
            <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6.5 3.8l2.6 1 1 3.1-1.8 1.6a11 11 0 0 0 5.2 5.2l1.6-1.8 3.1 1 1 2.6-1.6 1.9c-.8.9-2.1 1-3.2.5A17.6 17.6 0 0 1 4.4 8.6c-.5-1.1-.4-2.4.5-3.2z" fill="currentColor"/></svg>
          </button>
        </span>
        <span class="fb-26__cap">live · infinite</span>
      </div>
      <div class="fb-26__cell">
        <button class="fb-26__fab fb-26__fab--shadow" type="button" aria-label="Highlighted action, three pulses">
          <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 3l2.4 6.2H21l-5.3 3.9 2 6.4-5.7-4-5.7 4 2-6.4L3 9.2h6.6z" fill="currentColor"/></svg>
        </button>
        <span class="fb-26__cap">onboarding · 3×</span>
      </div>
      <div class="fb-26__cell">
        <span class="fb-26__wrap">
          <i class="fb-26__radar" aria-hidden="true"></i>
          <button class="fb-26__fab fb-26__fab--radar" type="button" aria-label="Scanning for devices">
            <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="2.6" fill="currentColor"/><path d="M7.8 16.2a6 6 0 0 1 0-8.4M16.2 7.8a6 6 0 0 1 0 8.4M5 19a10 10 0 0 1 0-14M19 5a10 10 0 0 1 0 14" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
          </button>
        </span>
        <span class="fb-26__cap">radar sweep</span>
      </div>
    </div>
    <p class="fb-26__chip">2.4s cycle · delays 0 / .8 / 1.6s · stops under reduced motion</p>
  </div>
</section>
.fb-26 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  background: var(--fb-26-bg);
  --fb-26-bg: radial-gradient(100% 80% at 50% 0%,oklch(0.26 0.05 160),oklch(0.15 0.02 175) 70%);
  --fb-26-accent: oklch(0.78 0.17 155);
  --fb-26-ink: oklch(0.97 0.01 160);
  --fb-26-mut: oklch(0.76 0.02 160);
  font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
  color: var(--fb-26-ink);
  display: grid;
  place-items: center;
  padding: clamp(20px,5vw,56px);
}

.fb-26 *,
.fb-26 *::before,
.fb-26 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.fb-26__stage {
  width: min(100%,58ch);
  display: grid;
  gap: clamp(20px,3.4vw,32px);
  justify-items: center;
  text-align: center;
}

.fb-26__eyebrow {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11.5px;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--fb-26-accent);
}

.fb-26__title {
  font-size: clamp(26px,4.6vw,44px);
  line-height: 1.05;
  letter-spacing: -.03em;
  font-weight: 700;
  text-wrap: balance;
}

.fb-26__sub {
  max-width: 50ch;
  font-size: clamp(13.5px,1.7vw,16.5px);
  line-height: 1.62;
  color: var(--fb-26-mut);
  text-wrap: pretty;
}

.fb-26__row {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(28px,8vw,72px);
  justify-content: center;
  padding: 18px 0;
}

.fb-26__cell {
  display: grid;
  gap: 16px;
  justify-items: center;
}

.fb-26__cap {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11px;
  letter-spacing: .06em;
  color: var(--fb-26-mut);
}

.fb-26__wrap {
  position: relative;
  display: grid;
  place-items: center;
}

.fb-26__ring {
  position: absolute;
  width: 66px;
  height: 66px;
  border-radius: 50%;
  border: 2px solid var(--fb-26-accent);
  pointer-events: none;
  opacity: 0;
  animation: fb-26-ping 2.4s cubic-bezier(0,.55,.45,1) infinite;
}

.fb-26__ring:nth-child(2) {
  animation-delay: .8s;
}

.fb-26__ring:nth-child(3) {
  animation-delay: 1.6s;
}

@keyframes fb-26-ping {
  0% {
    scale: .85;
    opacity: .7;
  }

  70% {
    opacity: 0;
  }

  100% {
    scale: 2.2;
    opacity: 0;
  }
}

.fb-26__fab {
  position: relative;
  display: grid;
  place-items: center;
  inline-size: 66px;
  block-size: 66px;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  color: oklch(0.15 0.04 160);
  background: radial-gradient(120% 120% at 32% 26%,oklch(0.92 0.15 150),var(--fb-26-accent));
  box-shadow: 0 16px 38px -14px oklch(0.78 0.17 155/.6),0 2px 6px oklch(0.08 0.01 175/.7);
  transition: transform .28s cubic-bezier(.2,1.3,.4,1),box-shadow .3s ease;
}

.fb-26__fab svg {
  width: 27px;
  height: 27px;
}

.fb-26__fab:hover {
  transform: translateY(-3px) scale(1.04);
}

.fb-26__fab:active {
  transform: scale(.93);
}

.fb-26__fab:focus-visible {
  outline: 3px solid oklch(0.98 0.01 160);
  outline-offset: 6px;
}

.fb-26__fab--shadow {
  background: radial-gradient(120% 120% at 32% 26%,oklch(0.9 0.14 95),oklch(0.78 0.16 78));
  color: oklch(0.2 0.05 78);
  animation: fb-26-shadow 2.2s cubic-bezier(0,.55,.45,1) 3;
}

@keyframes fb-26-shadow {
  0% {
    box-shadow: 0 0 0 0 oklch(0.78 0.16 78/.65),0 16px 38px -14px oklch(0.78 0.16 78/.6);
  }

  70%,
    100% {
    box-shadow: 0 0 0 22px oklch(0.78 0.16 78/0),0 16px 38px -14px oklch(0.78 0.16 78/.6);
  }
}

.fb-26__fab--radar {
  background: radial-gradient(120% 120% at 32% 26%,oklch(0.85 0.13 200),oklch(0.66 0.15 210));
  color: oklch(0.14 0.04 210);
}

.fb-26__radar {
  position: absolute;
  width: 132px;
  height: 132px;
  border-radius: 50%;
  pointer-events: none;
  background: conic-gradient(from 0deg,transparent 0deg,color-mix(in oklch,oklch(0.7 0.15 205) 55%,transparent) 42deg,transparent 60deg);
  -webkit-mask: radial-gradient(circle,transparent 32px,#000 33px,#000 66px,transparent 67px);
  mask: radial-gradient(circle,transparent 32px,#000 33px,#000 66px,transparent 67px);
  animation: fb-26-sweep 3.4s linear infinite;
}

@keyframes fb-26-sweep {
  to {
    rotate: 360deg;
  }
}

.fb-26__chip {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11px;
  color: var(--fb-26-mut);
  padding: 8px 14px;
  border: 1px solid oklch(1 0 0/.16);
  border-radius: 99px;
}

@media (prefers-reduced-motion: reduce) {
  .fb-26__ring,
    .fb-26__radar,
    .fb-26__fab--shadow {
    animation: none !important;
  }

  .fb-26__ring:first-of-type {
    opacity: .35;
    scale: 1.35;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Floating 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: Pulse Ring Attention-Getter Floating Button CSS
Source: https://codefronts.com/components/css-floating-buttons/fb03/

Three expanding rings on a stagger, the pattern used for call buttons, live indicators and onboarding spotlights. The important part is restraint: this demo shows the correct version (a short burst that stops), the radar variant, and a live-status dot — plus exactly why an infinite pulse on a persistent control is a mistake and when it is genuinely justified.
## HTML
```html
<section class="fb-26" aria-label="Pulse ring attention getter button demo">
  <div class="fb-26__stage">
    <p class="fb-26__eyebrow">staggered rings · opacity leads scale</p>
    <h2 class="fb-26__title">Pulse with a reason</h2>
    <p class="fb-26__sub">Three rings, each a third of a cycle behind the last. Fade to zero before the scale ends, or the ring pops instead of dissolving.</p>
    <div class="fb-26__row">
      <div class="fb-26__cell">
        <span class="fb-26__wrap">
          <i class="fb-26__ring" aria-hidden="true"></i><i class="fb-26__ring" aria-hidden="true"></i><i class="fb-26__ring" aria-hidden="true"></i>
          <button class="fb-26__fab" type="button" aria-label="Start a call">
            <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M6.5 3.8l2.6 1 1 3.1-1.8 1.6a11 11 0 0 0 5.2 5.2l1.6-1.8 3.1 1 1 2.6-1.6 1.9c-.8.9-2.1 1-3.2.5A17.6 17.6 0 0 1 4.4 8.6c-.5-1.1-.4-2.4.5-3.2z" fill="currentColor"/></svg>
          </button>
        </span>
        <span class="fb-26__cap">live · infinite</span>
      </div>
      <div class="fb-26__cell">
        <button class="fb-26__fab fb-26__fab--shadow" type="button" aria-label="Highlighted action, three pulses">
          <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M12 3l2.4 6.2H21l-5.3 3.9 2 6.4-5.7-4-5.7 4 2-6.4L3 9.2h6.6z" fill="currentColor"/></svg>
        </button>
        <span class="fb-26__cap">onboarding · 3×</span>
      </div>
      <div class="fb-26__cell">
        <span class="fb-26__wrap">
          <i class="fb-26__radar" aria-hidden="true"></i>
          <button class="fb-26__fab fb-26__fab--radar" type="button" aria-label="Scanning for devices">
            <svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><circle cx="12" cy="12" r="2.6" fill="currentColor"/><path d="M7.8 16.2a6 6 0 0 1 0-8.4M16.2 7.8a6 6 0 0 1 0 8.4M5 19a10 10 0 0 1 0-14M19 5a10 10 0 0 1 0 14" fill="none" stroke="currentColor" stroke-width="1.7" stroke-linecap="round"/></svg>
          </button>
        </span>
        <span class="fb-26__cap">radar sweep</span>
      </div>
    </div>
    <p class="fb-26__chip">2.4s cycle · delays 0 / .8 / 1.6s · stops under reduced motion</p>
  </div>
</section>
```
## CSS
```css
.fb-26 {
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  background: var(--fb-26-bg);
  --fb-26-bg: radial-gradient(100% 80% at 50% 0%,oklch(0.26 0.05 160),oklch(0.15 0.02 175) 70%);
  --fb-26-accent: oklch(0.78 0.17 155);
  --fb-26-ink: oklch(0.97 0.01 160);
  --fb-26-mut: oklch(0.76 0.02 160);
  font-family: 'Segoe UI',system-ui,-apple-system,sans-serif;
  color: var(--fb-26-ink);
  display: grid;
  place-items: center;
  padding: clamp(20px,5vw,56px);
}

.fb-26 *,
.fb-26 *::before,
.fb-26 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.fb-26__stage {
  width: min(100%,58ch);
  display: grid;
  gap: clamp(20px,3.4vw,32px);
  justify-items: center;
  text-align: center;
}

.fb-26__eyebrow {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11.5px;
  letter-spacing: .16em;
  text-transform: uppercase;
  color: var(--fb-26-accent);
}

.fb-26__title {
  font-size: clamp(26px,4.6vw,44px);
  line-height: 1.05;
  letter-spacing: -.03em;
  font-weight: 700;
  text-wrap: balance;
}

.fb-26__sub {
  max-width: 50ch;
  font-size: clamp(13.5px,1.7vw,16.5px);
  line-height: 1.62;
  color: var(--fb-26-mut);
  text-wrap: pretty;
}

.fb-26__row {
  display: flex;
  flex-wrap: wrap;
  gap: clamp(28px,8vw,72px);
  justify-content: center;
  padding: 18px 0;
}

.fb-26__cell {
  display: grid;
  gap: 16px;
  justify-items: center;
}

.fb-26__cap {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11px;
  letter-spacing: .06em;
  color: var(--fb-26-mut);
}

.fb-26__wrap {
  position: relative;
  display: grid;
  place-items: center;
}

.fb-26__ring {
  position: absolute;
  width: 66px;
  height: 66px;
  border-radius: 50%;
  border: 2px solid var(--fb-26-accent);
  pointer-events: none;
  opacity: 0;
  animation: fb-26-ping 2.4s cubic-bezier(0,.55,.45,1) infinite;
}

.fb-26__ring:nth-child(2) {
  animation-delay: .8s;
}

.fb-26__ring:nth-child(3) {
  animation-delay: 1.6s;
}

@keyframes fb-26-ping {
  0% {
    scale: .85;
    opacity: .7;
  }

  70% {
    opacity: 0;
  }

  100% {
    scale: 2.2;
    opacity: 0;
  }
}

.fb-26__fab {
  position: relative;
  display: grid;
  place-items: center;
  inline-size: 66px;
  block-size: 66px;
  border: 0;
  border-radius: 50%;
  cursor: pointer;
  color: oklch(0.15 0.04 160);
  background: radial-gradient(120% 120% at 32% 26%,oklch(0.92 0.15 150),var(--fb-26-accent));
  box-shadow: 0 16px 38px -14px oklch(0.78 0.17 155/.6),0 2px 6px oklch(0.08 0.01 175/.7);
  transition: transform .28s cubic-bezier(.2,1.3,.4,1),box-shadow .3s ease;
}

.fb-26__fab svg {
  width: 27px;
  height: 27px;
}

.fb-26__fab:hover {
  transform: translateY(-3px) scale(1.04);
}

.fb-26__fab:active {
  transform: scale(.93);
}

.fb-26__fab:focus-visible {
  outline: 3px solid oklch(0.98 0.01 160);
  outline-offset: 6px;
}

.fb-26__fab--shadow {
  background: radial-gradient(120% 120% at 32% 26%,oklch(0.9 0.14 95),oklch(0.78 0.16 78));
  color: oklch(0.2 0.05 78);
  animation: fb-26-shadow 2.2s cubic-bezier(0,.55,.45,1) 3;
}

@keyframes fb-26-shadow {
  0% {
    box-shadow: 0 0 0 0 oklch(0.78 0.16 78/.65),0 16px 38px -14px oklch(0.78 0.16 78/.6);
  }

  70%,
    100% {
    box-shadow: 0 0 0 22px oklch(0.78 0.16 78/0),0 16px 38px -14px oklch(0.78 0.16 78/.6);
  }
}

.fb-26__fab--radar {
  background: radial-gradient(120% 120% at 32% 26%,oklch(0.85 0.13 200),oklch(0.66 0.15 210));
  color: oklch(0.14 0.04 210);
}

.fb-26__radar {
  position: absolute;
  width: 132px;
  height: 132px;
  border-radius: 50%;
  pointer-events: none;
  background: conic-gradient(from 0deg,transparent 0deg,color-mix(in oklch,oklch(0.7 0.15 205) 55%,transparent) 42deg,transparent 60deg);
  -webkit-mask: radial-gradient(circle,transparent 32px,#000 33px,#000 66px,transparent 67px);
  mask: radial-gradient(circle,transparent 32px,#000 33px,#000 66px,transparent 67px);
  animation: fb-26-sweep 3.4s linear infinite;
}

@keyframes fb-26-sweep {
  to {
    rotate: 360deg;
  }
}

.fb-26__chip {
  font-family: ui-monospace,Menlo,Consolas,monospace;
  font-size: 11px;
  color: var(--fb-26-mut);
  padding: 8px 14px;
  border: 1px solid oklch(1 0 0/.16);
  border-radius: 99px;
}

@media (prefers-reduced-motion: reduce) {
  .fb-26__ring,
    .fb-26__radar,
    .fb-26__fab--shadow {
    animation: none !important;
  }

  .fb-26__ring:first-of-type {
    opacity: .35;
    scale: 1.35;
  }
}
```

How this works

Rings are pseudo-elements scaling out with a fading opacity, each offset by a third of the cycle:

@keyframes ping{ 0%{ scale:.85; opacity:.7 } 70%{ opacity:0 } 100%{ scale:2.1; opacity:0 } }
.ring   { animation:ping 2.4s cubic-bezier(0,.55,.45,1) infinite }
.ring:nth-child(2){ animation-delay:.8s }
.ring:nth-child(3){ animation-delay:1.6s }

Opacity must reach zero before the scale finishes (70% here), otherwise the ring pops out of existence at full size. That single offset is what makes the falloff read as a wave rather than a flicker.

Two cheaper variants are included. The box-shadow pulse animates a spread-only shadow from 0 to 18px — one element, no extra DOM, but it cannot fade independently of the button. The radar sweep is a conic-gradient rotating behind a masked circle, for "scanning" rather than "attention".

On restraint: an infinite pulse is defensible for a live state (recording, an active call, a device pairing) because the animation is the status. For a plain create button it is nagging — use three iterations on first render and stop.

Make it yours

  • Iteration count: animation-iteration-count:3 for onboarding, infinite only for genuine live status.
  • Ring count: two rings read calm, three read urgent, four read broken. Space delays evenly across the duration.
  • Colour fade: animate the border colour from the accent to transparent alongside opacity for a softer edge.
  • Speed: 2.4s is a heartbeat. Under 1.2s it reads as an alarm — appropriate for an emergency call button, wrong for anything else.

Gotchas — read before shipping

  • Rings must be pointer-events:none or the expanded ring intercepts clicks well outside the button's visual bounds.
  • Animating box-shadow spread repaints on every frame; animating scale on a pseudo-element stays on the compositor. Prefer the latter for persistent pulses.
  • Infinite motion near text is a known trigger for vestibular discomfort. Always provide the reduced-motion stop, and consider a static ring instead.
  • A pulse plus a badge plus a colour change is three attention signals fighting each other. Pick one.

Browser support

ChromeSafariFirefoxEdge
114+17.5+121+114+

Floor set by oklch(), color-mix(), text-wrap as this demo is written. The core technique itself goes back further — Chrome 111+.

Keyframes, scale and conic-gradient (Chrome 69+, Safari 12.1+, Firefox 83+) are all broadly supported. oklch() tokens are the only modern floor.

Search CodeFronts

Loading…