A call-to-action button that sends soft rings outward to ask for attention, calming as soon as it is hovered or focused, and switchable off once the person acts.
import { PulseButton } from "@/components/ballmac/pulse-button"
export default function PulseButtonDemo() {
return (
<PulseButton size="lg" rings={3}>
Start your free trial
</PulseButton>
)
}Installation
$ pnpm dlx shadcn@latest add @ballmac/pulse-buttonInstall the dependencies.
$ pnpm add motion@^12Add the Ballmac items it builds on.
$ pnpm dlx shadcn@latest add @ballmac/buttonCopy the source into your project.
components/ballmac/pulse-button.tsx// Ballmac UI: Pulse Button. https://ui.ballmac.com/components/pulse-button "use client" import * as React from "react" import { motion, useReducedMotion } from "motion/react" import { Button, type ButtonProps } from "@/components/ballmac/button" import { cn } from "@/lib/utils" type PulseTone = "primary" | "chart-1" | "chart-2" | "chart-3" | "chart-4" | "chart-5" | "destructive" const RING: Record<PulseTone, string> = { primary: "bg-primary", "chart-1": "bg-chart-1", "chart-2": "bg-chart-2", "chart-3": "bg-chart-3", "chart-4": "bg-chart-4", "chart-5": "bg-chart-5", destructive: "bg-destructive", } type PulseButtonProps = Omit<ButtonProps, "asChild"> & { /** Color of the rings. */ tone?: PulseTone /** How many rings leave the button, one after another. */ rings?: number /** Seconds one ring takes to fade out. */ duration?: number /** Turn the pulse off, for example once the person has acted. */ active?: boolean /** Classes for the wrapper around the button and rings. */ wrapperClassName?: string } function PulseButton({ tone = "primary", rings = 2, duration = 2, active = true, shape, disabled, wrapperClassName, className, children, ...props }: PulseButtonProps) { const reduce = useReducedMotion() const [calm, setCalm] = React.useState(false) const pulsing = active && !disabled && !calm const radius = shape === "pill" ? "rounded-full" : "rounded-md" return ( <span data-slot="pulse-button" data-pulsing={pulsing || undefined} onPointerEnter={() => setCalm(true)} onPointerLeave={() => setCalm(false)} onFocusCapture={() => setCalm(true)} onBlurCapture={() => setCalm(false)} className={cn("relative inline-flex", wrapperClassName)} > {active && !disabled && Array.from({ length: rings }, (_, i) => ( <motion.span key={i} aria-hidden="true" className={cn("pointer-events-none absolute inset-0 -z-10", radius, RING[tone])} initial={false} animate={reduce ? { scale: 1.12, opacity: 0.18 } : pulsing ? { scale: [1, 1.5], opacity: [0.35, 0] } : { scale: 1, opacity: 0 }} transition={reduce ? { duration: 0 } : pulsing ? { duration, delay: (i * duration) / rings, repeat: Infinity, ease: "easeOut" } : { duration: 0.2 }} /> ))} <Button shape={shape} disabled={disabled} className={cn("relative z-0", className)} {...props}> {children} </Button> </span> ) } export { PulseButton, type PulseButtonProps, type PulseTone }Update the import paths to match your project setup.
Usage
import { PulseButton } from "@/components/ballmac/pulse-button"The full example is in the Code tab above.
Examples
Tones and states
"use client"
import * as React from "react"
import { PulseButton } from "@/components/ballmac/pulse-button"
export default function PulseButtonTones() {
const [recording, setRecording] = React.useState(true)
return (
<div className="flex flex-wrap items-center justify-center gap-6">
<PulseButton tone="chart-2" shape="pill" onClick={() => setRecording((r) => !r)} active={recording}>
{recording ? "Recording…" : "Record"}
</PulseButton>
<PulseButton tone="destructive" variant="destructive">
Live now
</PulseButton>
<PulseButton tone="chart-1" variant="outline" disabled>
Disabled
</PulseButton>
</div>
)
}API reference
| Prop | Type | Default |
|---|---|---|
toneColor of the rings. | PulseTone | "primary" |
ringsHow many rings leave the button, one after another. | number | 2 |
durationSeconds one ring takes to fade out. | number | 2 |
activeTurn the pulse off, for example once the person has acted. | boolean | true |
wrapperClassNameClasses for the wrapper around the button and rings. | string | — |
Also accepts the standard attributes of its root element.
Accessibility
| Key | Action |
|---|---|
| Enter / Space | Activates like any button |
| Screen readers | Rings are aria-hidden |
| Reduced motion | A still faint ring replaces the pulse |
Use with AI
<PulseButton tone rings duration active>Label</PulseButton>. Rings stop on hover and focus, and when active is false or the button is disabled. With the shadcn MCP server set up (guide), ask your agent:
Add the Ballmac UI Pulse Button (@ballmac/pulse-button) to this project with the shadcn MCP, then use it where it fits.
Use it for
- Onboarding steps and 'try it now' prompts
- Live or record buttons
Not for
- More than one per view
- Anything permanent: attention fades if it never stops
Registry JSON: https://ui.ballmac.com/r/pulse-button.json
Credits
Free to use in personal and commercial projects.
- npm
- motion@^12
- Registry
- @ballmac/buttonshadcn/utils
Pairs well with
Button
A button with six variants, three sizes, a pill shape and a built-in loading state. buttonVariants() styles links the same way.
Shiny Button
A button with a diagonal band of light that sweeps across on hover and focus, or on a timer to draw the eye, built on the Ballmac button.
Status Dot
A compact status indicator that always pairs its color with a visible label.
Animated Beam
An SVG beam that connects two elements with a curved path and sends a glowing gradient pulse along it. Follows layout changes, pauses off-screen, static under reduced motion.