Skip to content
caliper ui docs

Components

Ripple

Ink that spreads from where a surface is pressed. Drop it inside anything pressable; it listens to its parent. A held press keeps the ink until release, and Enter or Space start it from the centre.

Free
pnpm dlx shadcn@latest add @caliper/ripple

Choose a plan

Press and hold a plan to see the ink stay.

Installs components/ui/ripple.tsxDepends on none beyond ReactMotion honours reduced motion

When to use

Use it for

  • Large pressable surfaces such as tiles, rows and cards, where a press needs a visible answer.
  • Touch-first screens, where hover never happens.

Skip it when

  • Buttons that already have a press state; two answers to one press is noise.
  • Text links.

Decisions

Why it is built this way, so you know what to keep when you change your copy.

Behaviour

Hold keeps the ink
The ink grows to cover the surface and stays while held, then fades on release, so a long press is visibly a press.
Reduced motion
The spread is replaced by a brief tint over the whole surface.

Styling

Current colour, faint
It uses the text colour at 14%, so it suits any surface and never competes with the label.

Installation

terminal
pnpm dlx shadcn@latest add @caliper/ripple

Adds its 2 files to components/ui/, each with its own stylesheet, and installs their packages. No account needed. The first part also adds Caliper UI’s tokens; import them once as in Installation.

Usage

This is the code behind the preview above.

example.tsx
import { Check } from "lucide-react"
import { useState, type ReactNode } from "react"
import { Ripple } from "@/components/ui/ripple"

function Panel({ title, context, className = "max-w-md", children }: { title: string; context: string; className?: string; children: ReactNode }) {
  return (
    <section className={`grid w-full gap-5 rounded-overlay border border-border bg-background p-6 shadow-resting max-sm:p-4 ${className}`}>
      <div className="grid gap-1">
        <p className="font-medium">{title}</p>
        <p className="text-sm text-muted-foreground">{context}</p>
      </div>
      {children}
    </section>
  );
}

export function RippleDemo() {
  const [plan, setPlan] = useState("team");
  const plans = [
    { id: "solo", name: "Solo", price: "$12", note: "One person, every part" },
    { id: "team", name: "Team", price: "$39", note: "Up to 10 seats, shared library" },
    { id: "studio", name: "Studio", price: "$89", note: "Unlimited seats, priority help" },
  ];
  return (
    <Panel title="Choose a plan" context="Press and hold a plan to see the ink stay." className="max-w-md">
      <div role="radiogroup" aria-label="Plan" className="grid gap-2">
        {plans.map((item) => (
          <button
            key={item.id}
            type="button"
            role="radio"
            aria-checked={plan === item.id}
            onClick={() => setPlan(item.id)}
            className="relative flex items-center gap-3 overflow-hidden rounded-surface border border-border px-4 py-3 text-left outline-none focus-visible:outline-2 focus-visible:outline-ring aria-checked:border-foreground"
          >
            <span className="min-w-0 flex-1">
              <span className="block font-medium">{item.name}</span>
              <span className="block text-sm text-muted-foreground">{item.note}</span>
            </span>
            <span className="text-sm tabular-nums">{item.price}/mo</span>
            <span className={`grid size-5 place-items-center rounded-full ${plan === item.id ? "bg-foreground text-background" : "border border-border"}`}>{plan === item.id && <Check aria-hidden className="size-3" />}</span>
            <Ripple />
          </button>
        ))}
      </div>
    </Panel>
  );
}

API reference

Ripple

component

Also accepts the native props of its root element and a ref.

PropTypeDefaultDescription
disabledbooleanfalseTurns the ripple off, for a disabled host.

Accessibility

It is built on native elements and keeps aria-hidden in step with state.

KeyBehaviour
EnterActivates the focused control.
SpaceActivates the focused control.

Motion

It checks prefers-reduced-motion itself and settles immediately instead of travelling. Every state is still reachable.

All Surfaces examples