Ripple

Expands a circle from the point that was pressed. The only decorator of the kit that renders a node, and the only one that holds state.

API

Props

proptypedefault
children?ReactElementthe element to ripple; cloned, its children extended
rippleClassName?string"bg-primary-darken"classes of the circle, usually its colour

Examples

Usage

tsx
<Ripple>
  <button className="rounded-lg border-2 border-border px-3 py-2">press me</button>
</Ripple>
previewpress the surfaces

Details

Where it departs from the others

The other decorators are pure CSS. A ripple cannot be: it needs one element per press, positioned where the pointer landed, animated once and then discarded. So Ripple keeps the current ripple in state and appends a span to the children of its child — it still clones rather than wraps, but it does add a node.

It also means the child must accept children of its own, on top of the usual className and onPointerDown. The clone sets relative overflow-hidden on it, which is what clips the circle to the shape of the element, rounded corners included.

The geometry

The circle is placed at the press point and sized to twice the diagonal of the element, which guarantees it covers the surface from any corner — the farthest point of a rectangle from any position inside it is at most its diagonal.

tsx
size = Math.hypot(bounds.width, bounds.height) * 2;
x = event.clientX - bounds.left;
y = event.clientY - bounds.top;

From there a 500ms animation scales it from zero to full while fading it from half opacity to nothing, and the ripple removes itself on animationend. Each press gets a fresh key, so pressing again restarts the animation instead of resuming it.

Colour and contrast

rippleClassName is a class list rather than a colour prop, so it takes any utility — an opacity, a gradient, a blend mode. The default is the darker primary, which reads on the kit's surfaces; over a coloured or inverted background, pick something with the contrast you need.

tsx
<Ripple rippleClassName="bg-on-main/30">
  <Card as="button"></Card>
</Ripple>

Button already wraps itself in a Ripple, so applying one over a Button would give you two circles. Reach for it on surfaces that are pressable but are not Buttons — a card acting as a control, a custom row.

Pointer, not keyboard

The ripple starts on pointerdown, so it is feedback for a press and does not fire when a control is activated with the keyboard. That is deliberate — it is an animation about where you touched — but it means a ripple is never the only feedback a control should have.