Pulse Button — Free HTML CSS JS Sonar Pulse Snippet
Pulse Button · Buttons · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Pulse Button — Sonar Rings That Draw the Eye to an Action

The pulse button is the attention-grabbing call to action that emits expanding rings like sonar — perfect for "Go live," "Record," or any action you want users to notice. This snippet builds it with plain HTML, CSS, and a little vanilla JavaScript for a live/stop toggle and efficient off-screen pausing.
The expanding pulse rings
Behind the button label sit two ring elements that share the button shape (inset: 0, border-radius: inherit). The pbPulse keyframe scales each ring from 1 to 1.7 while fading its opacity from .55 to 0, so it grows outward and dissolves — the sonar ping. Because the rings inherit the button border radius, the pulse takes the button shape rather than a plain circle, which looks more integrated. The rings sit at z-index: 0 behind the label so the text stays crisp on top.
Staggering for a continuous ripple
A single ring would pulse, pause, then pulse again with a visible gap. Two rings with the second offset by animation-delay: 1s (half the 2-second loop) means a new ring is always emerging as the previous one finishes — so the effect is a continuous, overlapping ripple rather than a blinking single pulse. This stagger-by-half-the-duration is the standard trick for seamless looping emitters.
A meaningful toggle
The button is not just decorative — clicking it toggles a live state. When live, it shows "● Go live" with a red gradient and pulsing rings; clicking flips it to "■ Stop" with a muted gray gradient, removes the shadow, and pauses the rings via a .stopped class. This mirrors real broadcast or recording controls, where the pulse communicates an active state and stopping it calms the button. Wire the toggle to your actual start/stop logic and the visual state follows.
Pausing off-screen
Continuously scaling, fading rings cost compositing work, so an IntersectionObserver watches the button and pauses the ring animations whenever the button scrolls out of view (and only resumes them if it is both visible and live). With threshold: 0 it reacts as soon as any part leaves the viewport. This keeps a page with a persistent pulsing CTA from doing animation work nobody can see.
Why CSS rings over JavaScript
The pulse is pure CSS keyframes, so it runs on the compositor without any per-frame scripting — the JavaScript only flips classes and styles on discrete events (click, visibility change). That keeps the effect smooth and cheap even if several pulse buttons appear on a page.
Customizing it
Change the scale target and opacity curve for a larger or subtler pulse, retime the loop and adjust the second ring delay to match, add a third ring for a denser ripple, recolor the rings and gradients, or change the labels and states. Use it for notifications, record buttons, or live indicators. Pair it with a shiny text badge or a border beam card to coordinate attention cues.
Build with AI
Build, Understand, Optimize, and Extend It With AI
You do not need to work out the ring timing by hand. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why the second ring's animation-delay is set to half the pbPulse loop duration and what would happen visually if that offset were mismatched with the loop length. The same assistant is useful for optimizing it — ask whether the IntersectionObserver threshold of 0 is the right choice if the button sits inside a scrollable panel rather than the main page, or whether toggling animation-play-state is cheaper than adding and removing the ring elements outright. It is just as handy for extending the effect — ask it to add a third staggered ring for a denser ripple, drive the live/stopped state from a real WebSocket connection instead of a click, or expose the ring color and scale target as CSS custom properties so multiple buttons on one page can each pulse a different hue. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a "pulse button" call-to-action in plain HTML, CSS, and JavaScript using only CSS keyframe animations and the IntersectionObserver API — no animation libraries.
Requirements:
- A button containing a text label plus two absolutely positioned ring elements behind it (inset: 0, border-radius: inherit so the rings take the button's exact shape, not a plain circle), with the label kept above the rings via z-index.
- A single keyframe animation that scales each ring from 1 to roughly 1.7 while fading its opacity from about 0.55 to 0, running on an infinite 2-second ease-out loop.
- The second ring must reuse the exact same animation but with an animation-delay equal to half the loop duration (1s for a 2s loop), so a new ring is always emerging as the previous one fades out, producing a continuous overlapping ripple instead of a pulse-then-gap pattern.
- Clicking the button must toggle a live/stopped state: swap the label text and icon, switch the button's background gradient to a muted tone, remove its box-shadow, and add a class that pauses both rings via animation-play-state.
- Use an IntersectionObserver watching the button (threshold: 0) to pause the ring animations whenever the button scrolls out of view, and only resume them if the button is both visible and in the live state — so no compositing work happens for a pulse nobody can see.
- Do not implement the pulsing in JavaScript (no per-frame opacity/scale updates) — the ring motion must be pure CSS running on the compositor, with JavaScript limited to toggling classes and inline styles on discrete events.Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.
Step by step
How to Use
- 1Paste HTML, CSS, and JSA red Go live button emits expanding sonar rings.
- 2Watch the rippleTwo staggered rings keep a continuous pulse going.
- 3Click the buttonIt toggles to a muted Stop state and the pulse halts.
- 4Click againIt returns to the live, pulsing state.
- 5Scroll it awayThe rings pause off-screen to save work.
- 6Recolor and retimeAdjust the ring scale, colors, and loop.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
Two ring elements run the same pulse keyframe, but the second has an animation-delay of half the loop duration. That means a new ring is always emerging as the previous one finishes, so instead of one ring pulsing with a visible gap, the rings overlap into a continuous ripple. Staggering by half the duration is the standard trick for seamless emitters.
The rings use inset: 0 and border-radius: inherit, so they take the button rounded-rectangle shape rather than a plain circle. As the pbPulse keyframe scales and fades them, the pulse grows outward in the button shape, which looks more integrated than a generic circular ping.
Yes. Clicking toggles a live state: live shows a red Go live with pulsing rings, and stopped shows a muted Stop with the rings paused and the shadow removed via a class. It mirrors a broadcast or record control, and you can wire the toggle to your real start/stop logic so the visual state tracks it.
Yes. The pulse is pure CSS keyframes running on the compositor, and an IntersectionObserver pauses the ring animations whenever the button is off-screen, resuming only when it is both visible and live. So a persistent pulsing CTA does no animation work while it is scrolled out of view.
Render the button with its ring elements and keep the live state in component state, toggling a class and the label on click. Put the IntersectionObserver in a mount effect with cleanup on unmount. The pulse keyframes are pure CSS. In Tailwind, define the pulse animation in the config and apply it to the ring spans with a delayed variant for the second ring.