Skip to content
caliper ui docs

Components

Push button

A chunky button with visible depth: the face sits on its own edge, lifts a pixel on hover, travels down onto the edge when pressed and springs back when let go. For playful flows such as onboarding, lessons and rewards.

Free
pnpm dlx shadcn@latest add @caliper/push-button

Lesson complete

12 new words · 4 minutes · no mistakes

+20

points today

Installs components/ui/push-button.tsxDepends on none beyond ReactMotion none in component source

When to use

Use it for

  • Game-like moments: continue a lesson, claim a reward, finish a setup.
  • Kiosk and touch screens, where a press should feel physical.

Skip it when

  • Ordinary product actions; use `Button`, which keeps the calm product material.
  • More than one on a screen as a primary; depth draws the eye.

Decisions

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

Behaviour

Fast down, springy up
The press is almost instant so it feels like contact; the release uses the settle curve, which overshoots a little like a key returning.
A real button
It is a native button with its own focus ring; a disabled one sits pressed and grey.

Styling

Edge from the face
The edge is the face colour mixed with black, so any variant or theme keeps a believable side.

Installation

terminal
pnpm dlx shadcn@latest add @caliper/push-button

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 { ArrowRight } from "lucide-react"
import { type ReactNode } from "react"
import { PushButton } from "@/components/ui/push-button"

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 PushButtonDemo() {
  return (
    <Panel title="Lesson complete" context="12 new words · 4 minutes · no mistakes" className="max-w-sm">
      <div className="grid justify-items-center gap-1 rounded-surface bg-muted py-6 text-center">
        <p className="text-4xl font-bold tabular-nums">+20</p>
        <p className="text-sm text-muted-foreground">points today</p>
      </div>
      <div className="grid gap-3">
        <PushButton variant="accent" size="lg">
          Continue <ArrowRight aria-hidden />
        </PushButton>
        <div className="grid grid-cols-2 gap-3">
          <PushButton variant="secondary">Review</PushButton>
          <PushButton>Share</PushButton>
        </div>
        <PushButton disabled size="sm">
          Next lesson unlocks tomorrow
        </PushButton>
      </div>
    </Panel>
  );
}

API reference

PushButton

component

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

PropTypeDefaultDescription
variant"default" | "secondary" | "accent""default"Ink for the main action, accent for the one moment that should stand out, neutral for the rest.
size"sm" | "md" | "lg""md"

Accessibility

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

No custom key handling; Tab and the native activation keys do the work.

Motion

It declares no animation in its component source. Any transition comes from its stylesheet, which uses the duration tokens that collapse to instant under prefers-reduced-motion.

All Surfaces examples