Sonar
Pulses rings outward from a dot, badge or button to draw attention to it.
Quick Preview
Composition
Use this public composition when building Sonar:
Sonar
└── (one child: the dot, badge or button being drawn attention to)Usage
Sonar is a wrapper, not a control. It renders its child untouched and pulses rings outward from it.
<Sonar tone="danger"><Badge variant="danger">2 overdue</Badge></Sonar>The rings trace the child's own shape. Sonar reads the child's computed border-radius and hands it to the rings, so a circular dot pulses a circle, a pill badge pulses a pill, and a rounded button pulses a rounded rectangle — with nothing to keep in sync by hand.
That measurement is what makes it safe to wrap anything. Radii in this library are often size-dependent — Tag alone moves through rounded-md, rounded-lg and rounded-xl across its three sizes — so a shape written out by hand at the wrapper would silently drift the moment the child's size changed.
Each ring is painted once as a box-shadow spread, which follows the border radius exactly and grows the ring by the same number of pixels on every side, so one ring fits an 8px dot and a full-width button alike. The animation then only scales and fades that finished ring — transform and opacity are the two properties a browser can animate on the compositor, and growing the spread itself instead repaints the ring on every frame, which iOS Safari stutters and flashes through. The scale is worked out per axis from the measured child, so a wide element's ring still travels the same distance on every side rather than stretching sideways.
Shape it yourself
Pass radius when the child's shape can't be read off the DOM — an SVG that draws its own corners, say. It's used verbatim and skips measurement entirely.
<Sonar radius="9999px">
<MyCustomAvatar />
</Sonar>Clipping
box-shadow spread paints outside the element's box, so an ancestor with overflow: hidden clips the rings. Give the sonar room, or move the clip.
Backgrounds
The rings sit behind the child, with the child's own shape punched out of them, so nothing paints inside it. That holds whether the child has a background of its own or not — a tertiary-outline button pulses the same halo as a solid one, with a clean middle.
Examples
Tones
tone colours the rings from the status tokens. It is decoration, not information — a red sonar does not say "error" to anyone who cannot see it, so keep the meaning in the child.
Size and waves
size sets how far each ring travels, and inherits from SizeProvider. waves sets how many rings are in flight at once — they are spread evenly across one cycle, so more waves reads as a faster, more urgent sonar rather than a longer one.
size
waves
Turning it off
Set active={false} once the nudge has done its job. The child stays mounted and keeps its place in the layout, so nothing shifts when the rings go.
Props
| Prop/API | Type | Default | Description |
|---|---|---|---|
children | ReactNode | - | The element being drawn attention to. One element; its shape is what the rings trace. |
active | boolean | true | Pulse or not. false drops the rings and leaves the child alone. |
tone | "brand" | "info" | "success" | "warning" | "danger" | "brand" | Ring colour, from the status tokens. |
size | "sm" | "md" | "lg" | "md" | How far each ring travels (6px / 10px / 14px). Inherits from SizeProvider. |
waves | 1 | 2 | 3 | 2 | Rings in flight at once, spread evenly across one cycle. |
radius | string | - | A border-radius used verbatim, skipping measurement. |
className | string | - | Local layout or spacing overrides, applied to the wrapper. |
sonarWaveVariants | cva variants | - | Ring styling, for recipes that render their own. |
| native props | span props | - | Passed through to the wrapper. |
Accessibility
- The rings are
aria-hiddenand the child is passed through untouched, so whatever the child announces is what assistive tech hears. - A pulse is not a label. If the attention it draws carries meaning — "unread", "overdue", "new" — put that meaning in the child, where it can be read.
- Reduced motion keeps the cue and drops only the movement: the rings stop travelling and one settles into a resting halo. An attention marker that disappears entirely is worse than one that holds still.
- Nothing is focusable or clickable that was not already — the wrapper adds no interaction of its own.