A chunky, tactile 3D button that lifts a little on hover and presses down onto its base when clicked, like a real key.
Customize
Usage
import { PushButton } from "@/components/motionkit/push-button";
<PushButton>Press me</PushButton>Motion
The exact curves this component runs, plotted with Motion's own maths.
Lift
Hover
Press
Press
Playful brands, games, onboarding and kids' products, and any one big button that deserves to feel physical.
The button is two layers: a base in a darker mix of the face colour, made with color-mix so it works for any colour, and the face lifted above it by the depth. Hover raises the face two more pixels, and pressing drops it almost onto the base with a faster transition, which is what makes the press feel crisp. It's pure CSS, so it responds on the very first frame.
Works in the App Router as is. Requires Tailwind CSS v4.
One command adds the file and installs its dependencies. Needs a components.json (run npx shadcn@latest init once if you don't have one).
npx shadcn@latest add https://www.motionkitui.com/r/push-button.jsonnpm install motionSave as components/motionkit/push-button.tsx.
import type { CSSProperties, ComponentProps } from "react";
type PushButtonProps = ComponentProps<"button"> & {
/** Colour of the button's face. The sides are a darker shade of it. */
color?: string;
/** How tall the button stands, in pixels. */
depth?: number;
};
/**
* A chunky, tactile 3D button: it lifts a little on hover and presses down onto its base when clicked,
* like a real key. Pure CSS, so it responds instantly.
*/
export function PushButton({ color = "#0E7C86", depth = 6, className = "", children, style, ...props }: PushButtonProps) {
return (
<button
{...props}
style={{ "--face": color, "--depth": `${depth}px`, ...style } as CSSProperties}
className={`group relative inline-flex rounded-xl pt-[var(--depth)] outline-none focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-[#5FAEB4] disabled:pointer-events-none disabled:opacity-50 ${className}`}
>
{/* the base and sides: the face colour, darkened */}
<span aria-hidden className="absolute inset-x-0 bottom-0 top-[var(--depth)] rounded-xl bg-[color-mix(in_oklab,var(--face)_55%,black)]" />
<span
className="relative inline-flex -translate-y-[var(--depth)] items-center gap-2 rounded-xl bg-[var(--face)] px-6 py-3 text-sm font-semibold text-white shadow-[inset_0_1px_0_rgb(255_255_255/0.25)] transition-transform duration-150 ease-out group-hover:-translate-y-[calc(var(--depth)+2px)] group-active:-translate-y-[calc(var(--depth)*0.25)] group-active:duration-75 motion-reduce:transition-none"
>
{children}
</span>
</button>
);
}
| Prop | Type | Default | Description |
|---|---|---|---|
| color | string | "#0E7C86" | Face colour; the sides are a darker mix of it. |
| depth | number | 6 | How tall the button stands, in pixels. |
| ...props | ButtonHTMLAttributes | — | Anything a <button> accepts. |
How we audit: the WCAG 2.2 animation checklist and respecting reduced motion in React.
Yes. The file starts with "use client", so you can import it straight into a Server Component page or layout. It works in Vite as is, and in Astro as a React island.
Nothing beyond React and Tailwind CSS v4. There is no MotionKit package and no Tailwind config to add.
Yes. Run npx shadcn@latest add https://www.motionkitui.com/r/push-button.json in any project with a components.json. It saves the file to components/motionkit and installs its dependencies.
With reduced motion on, it changes position instantly rather than travelling.
Hold to Confirm
A button that fills while held, so destructive actions can never happen by accident.
Arrow Slide Button
A call to action whose arrow slides out on hover while a fresh one slides in from the left. Pure CSS, solid or outline.
Ripple Button
A button that sends a ripple out from exactly where it was pressed, or from the centre when pressed from the keyboard.