Sonar

Pulses rings outward from a dot, badge or button to draw attention to it.

Quick Preview

3 new

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.

BrandInfoSuccessWarningDanger

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/APITypeDefaultDescription
childrenReactNode-The element being drawn attention to. One element; its shape is what the rings trace.
activebooleantruePulse 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.
waves1 | 2 | 32Rings in flight at once, spread evenly across one cycle.
radiusstring-A border-radius used verbatim, skipping measurement.
classNamestring-Local layout or spacing overrides, applied to the wrapper.
sonarWaveVariantscva variants-Ring styling, for recipes that render their own.
native propsspan props-Passed through to the wrapper.

Accessibility

  • The rings are aria-hidden and 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.

On this page