3D Press Button
Original · freeChunky 3D buttons that depress on press.
Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.
npx shadcn@latest add https://webinnoventix.com/r/btn-3d-press.json"use client";
import {
useState,
type ButtonHTMLAttributes,
type ReactNode,
} from "react";
type ColorKey =
| "indigo"
| "violet"
| "emerald"
| "rose"
| "amber"
| "sky"
| "neutral";
type Size = "sm" | "md" | "lg";
const BASE =
"group relative inline-flex select-none items-center justify-center gap-2 font-semibold leading-none " +
"border-b-[6px] will-change-transform " +
"transition-[transform,background-color,border-color,filter] duration-100 ease-out " +
"translate-y-0 enabled:hover:-translate-y-px enabled:hover:border-b-[7px] " +
"enabled:active:translate-y-1 enabled:active:border-b-2 enabled:active:brightness-95 " +
"focus:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 " +
"focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-950 " +
"disabled:cursor-not-allowed disabled:opacity-60";
const COLORS: Record<ColorKey, string> = {
indigo:
"bg-indigo-500 enabled:hover:bg-indigo-400 border-indigo-700 dark:border-indigo-800 text-white focus-visible:ring-indigo-400",
violet:
"bg-violet-500 enabled:hover:bg-violet-400 border-violet-700 dark:border-violet-800 text-white focus-visible:ring-violet-400",
emerald:
"bg-emerald-500 enabled:hover:bg-emerald-400 border-emerald-700 dark:border-emerald-800 text-white focus-visible:ring-emerald-400",
rose:
"bg-rose-500 enabled:hover:bg-rose-400 border-rose-700 dark:border-rose-800 text-white focus-visible:ring-rose-400",
amber:
"bg-amber-400 enabled:hover:bg-amber-300 border-amber-600 dark:border-amber-700 text-amber-950 focus-visible:ring-amber-400",
sky:
"bg-sky-500 enabled:hover:bg-sky-400 border-sky-700 dark:border-sky-800 text-white focus-visible:ring-sky-400",
neutral:
"bg-white enabled:hover:bg-slate-50 border-slate-300 text-slate-900 " +
"dark:bg-slate-800 dark:enabled:hover:bg-slate-700 dark:border-slate-950 dark:text-slate-100 focus-visible:ring-slate-400",
};
const SIZES: Record<Size, string> = {
sm: "rounded-lg px-4 py-2 text-sm",
md: "rounded-xl px-5 py-2.5 text-base",
lg: "rounded-2xl px-7 py-3.5 text-lg",
};
interface Press3DProps extends ButtonHTMLAttributes<HTMLButtonElement> {
color?: ColorKey;
size?: Size;
children: ReactNode;
}
function Press3D({
color = "indigo",
size = "md",
className = "",
children,
...rest
}: Press3DProps) {
return (
<button
type="button"
className={`${BASE} ${SIZES[size]} ${COLORS[color]} ${className}`}
{...rest}
>
{children}
</button>
);
}
/* ---------- inline glyphs ---------- */
function HeartIcon({ filled, pop }: { filled: boolean; pop: boolean }) {
return (
<svg
viewBox="0 0 24 24"
className={`h-[1.15em] w-[1.15em] ${pop ? "btn3d-pop" : ""}`}
fill={filled ? "currentColor" : "none"}
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M12 21C-4 11 4 2 12 7c8-5 16 4 0 14Z" />
</svg>
);
}
function CheckIcon() {
return (
<svg
viewBox="0 0 24 24"
className="h-[1.15em] w-[1.15em] btn3d-pop"
fill="none"
stroke="currentColor"
strokeWidth={2.5}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="m4 12 5 5L20 6" />
</svg>
);
}
function CopyIcon() {
return (
<svg
viewBox="0 0 24 24"
className="h-[1.15em] w-[1.15em]"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<rect x="9" y="9" width="11" height="11" rx="2" />
<path d="M5 15V5a2 2 0 0 1 2-2h8" />
</svg>
);
}
function Spinner() {
return (
<svg
viewBox="0 0 24 24"
className="btn3d-spinner h-[1.15em] w-[1.15em]"
fill="none"
aria-hidden="true"
>
<circle
cx="12"
cy="12"
r="9"
stroke="currentColor"
strokeOpacity="0.3"
strokeWidth="3"
/>
<path
d="M21 12a9 9 0 0 0-9-9"
stroke="currentColor"
strokeWidth="3"
strokeLinecap="round"
/>
</svg>
);
}
function PlusIcon() {
return (
<svg
viewBox="0 0 24 24"
className="h-[1.15em] w-[1.15em]"
fill="none"
stroke="currentColor"
strokeWidth={2.5}
strokeLinecap="round"
aria-hidden="true"
>
<path d="M12 5v14M5 12h14" />
</svg>
);
}
function MinusIcon() {
return (
<svg
viewBox="0 0 24 24"
className="h-[1.15em] w-[1.15em]"
fill="none"
stroke="currentColor"
strokeWidth={2.5}
strokeLinecap="round"
aria-hidden="true"
>
<path d="M5 12h14" />
</svg>
);
}
function CartIcon() {
return (
<svg
viewBox="0 0 24 24"
className="h-[1.15em] w-[1.15em]"
fill="none"
stroke="currentColor"
strokeWidth={2}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<circle cx="9" cy="20" r="1.4" />
<circle cx="18" cy="20" r="1.4" />
<path d="M2 3h2.2l2.3 12.3a1.6 1.6 0 0 0 1.6 1.3h8.5a1.6 1.6 0 0 0 1.6-1.2L21 7H5.3" />
</svg>
);
}
function BoltIcon() {
return (
<svg
viewBox="0 0 24 24"
className="h-[1.15em] w-[1.15em]"
fill="currentColor"
aria-hidden="true"
>
<path d="M13 2 4 14h6l-1 8 9-12h-6z" />
</svg>
);
}
function ArrowIcon() {
return (
<svg
viewBox="0 0 24 24"
className="h-[1.15em] w-[1.15em] transition-transform duration-150 group-hover:translate-x-0.5"
fill="none"
stroke="currentColor"
strokeWidth={2.5}
strokeLinecap="round"
strokeLinejoin="round"
aria-hidden="true"
>
<path d="M5 12h14M13 6l6 6-6 6" />
</svg>
);
}
/* ---------- section ---------- */
function GroupLabel({ children }: { children: ReactNode }) {
return (
<p className="mb-4 text-xs font-bold uppercase tracking-[0.18em] text-slate-400 dark:text-slate-500">
{children}
</p>
);
}
const ICON_ONLY = "!px-3 !py-3 rounded-xl";
export default function Btn3dPress() {
const [liked, setLiked] = useState(false);
const [likes, setLikes] = useState(214);
const [copied, setCopied] = useState(false);
const [saving, setSaving] = useState(false);
const [qty, setQty] = useState(1);
const [added, setAdded] = useState(false);
const [power, setPower] = useState(true);
const onCopy = async () => {
try {
await navigator.clipboard.writeText("npm i motion");
} catch {
/* clipboard unavailable */
}
setCopied(true);
window.setTimeout(() => setCopied(false), 2000);
};
const onSave = () => {
setSaving(true);
window.setTimeout(() => setSaving(false), 1800);
};
const onAdd = () => {
setAdded(true);
window.setTimeout(() => setAdded(false), 1800);
};
const colors: ColorKey[] = [
"indigo",
"violet",
"emerald",
"sky",
"amber",
"rose",
"neutral",
];
return (
<section className="relative w-full bg-slate-50 px-6 py-20 text-slate-900 dark:bg-slate-950 dark:text-slate-100 sm:px-10">
<style>{`
.btn3d-spinner { animation: btn3d-spin 0.7s linear infinite; transform-origin: center; }
@keyframes btn3d-spin { to { transform: rotate(360deg); } }
.btn3d-pop { animation: btn3d-pop 0.35s ease-out; }
@keyframes btn3d-pop { 0% { transform: scale(1); } 40% { transform: scale(1.35); } 100% { transform: scale(1); } }
@media (prefers-reduced-motion: reduce) {
.btn3d-spinner { animation-duration: 1.3s; }
.btn3d-pop { animation: none; }
}
`}</style>
<div className="mx-auto max-w-4xl">
<header className="mb-14 text-center">
<span className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-white px-3 py-1 text-xs font-semibold uppercase tracking-widest text-slate-500 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-400">
<BoltIcon /> Tactile UI
</span>
<h2 className="mt-5 text-4xl font-black tracking-tight sm:text-5xl">
3D Press Buttons
</h2>
<p className="mx-auto mt-4 max-w-lg text-base text-slate-500 dark:text-slate-400">
Chunky, physical controls with a solid bottom edge that sinks on
press. Real state, keyboard-friendly, light and dark.
</p>
</header>
<div className="space-y-14">
{/* colors */}
<div>
<GroupLabel>Colours</GroupLabel>
<div className="flex flex-wrap items-end gap-4">
{colors.map((c) => (
<Press3D key={c} color={c}>
{c[0].toUpperCase() + c.slice(1)}
</Press3D>
))}
</div>
</div>
{/* sizes */}
<div>
<GroupLabel>Sizes</GroupLabel>
<div className="flex flex-wrap items-end gap-4">
<Press3D color="violet" size="sm">
Small
</Press3D>
<Press3D color="violet" size="md">
Medium
</Press3D>
<Press3D color="violet" size="lg">
Large
</Press3D>
</div>
</div>
{/* with icons */}
<div>
<GroupLabel>With icons</GroupLabel>
<div className="flex flex-wrap items-end gap-4">
<Press3D color="emerald">
<BoltIcon /> Deploy now
</Press3D>
<Press3D color="sky">
Continue <ArrowIcon />
</Press3D>
<Press3D color="indigo" className={ICON_ONLY} aria-label="Add item">
<PlusIcon />
</Press3D>
<Press3D color="neutral" className={ICON_ONLY} aria-label="Favourite">
<HeartIcon filled={false} pop={false} />
</Press3D>
</div>
</div>
{/* live interactive */}
<div>
<GroupLabel>Live & interactive</GroupLabel>
<div className="flex flex-wrap items-end gap-4">
{/* like toggle */}
<Press3D
color={liked ? "rose" : "neutral"}
aria-pressed={liked}
onClick={() => {
setLiked((v) => !v);
setLikes((n) => (liked ? n - 1 : n + 1));
}}
>
<HeartIcon filled={liked} pop={liked} />
<span className="tabular-nums">{likes}</span>
</Press3D>
{/* copy */}
<Press3D
color={copied ? "emerald" : "neutral"}
onClick={onCopy}
aria-label={copied ? "Copied to clipboard" : "Copy install command"}
className="min-w-[9.5rem]"
>
{copied ? (
<>
<CheckIcon /> Copied!
</>
) : (
<>
<CopyIcon /> npm i motion
</>
)}
</Press3D>
{/* loading */}
<Press3D
color="indigo"
onClick={onSave}
disabled={saving}
aria-busy={saving}
className="min-w-[9.5rem]"
>
{saving ? (
<>
<Spinner /> Saving…
</>
) : (
"Save changes"
)}
</Press3D>
{/* disabled */}
<Press3D color="violet" disabled>
Disabled
</Press3D>
</div>
</div>
{/* quantity + cart */}
<div>
<GroupLabel>Quantity & cart</GroupLabel>
<div className="flex flex-wrap items-center gap-4">
<div className="inline-flex items-center gap-3">
<Press3D
color="neutral"
className={ICON_ONLY}
aria-label="Decrease quantity"
disabled={qty <= 1}
onClick={() => setQty((q) => Math.max(1, q - 1))}
>
<MinusIcon />
</Press3D>
<span className="w-8 text-center text-xl font-bold tabular-nums">
{qty}
</span>
<Press3D
color="neutral"
className={ICON_ONLY}
aria-label="Increase quantity"
disabled={qty >= 10}
onClick={() => setQty((q) => Math.min(10, q + 1))}
>
<PlusIcon />
</Press3D>
</div>
<Press3D
color={added ? "emerald" : "amber"}
size="lg"
onClick={onAdd}
className="min-w-[15rem]"
>
{added ? (
<>
<CheckIcon /> Added {qty} to cart
</>
) : (
<>
<CartIcon /> Add {qty} · $79 each
</>
)}
</Press3D>
</div>
</div>
{/* power toggle */}
<div>
<GroupLabel>Toggle</GroupLabel>
<Press3D
color={power ? "emerald" : "neutral"}
aria-pressed={power}
onClick={() => setPower((v) => !v)}
className="min-w-[11rem]"
>
<span
className={`inline-block h-2.5 w-2.5 rounded-full ${
power ? "bg-white" : "bg-slate-400 dark:bg-slate-500"
}`}
aria-hidden="true"
/>
Power {power ? "On" : "Off"}
</Press3D>
</div>
</div>
</div>
</section>
);
}Dependencies
Licence
Built by Web Innoventix. Free for personal and commercial use, no attribution required.
Built by Web Innoventix
Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.
Get a free quoteSimilar components
Browse all →
Shimmer Button
MITA dark pill button with a continuous conic-gradient light shimmer sweeping around its border.

Rainbow Gradient Button
MITA button wrapped in an animated multi-colour gradient border with a matching blurred underglow.

Interactive Hover Button
MITA pill button whose dot expands to fill the surface and reveals an arrow label on hover.

Pulsating Button
MITA solid button that emits a soft, continuously pulsing glow ring to draw the eye to the primary action.

Shiny Button
MITA frosted-glass button with a spring-animated light sweeping across its text and border.

Solid Set Button
OriginalA set of solid buttons: primary, secondary, success and danger, in three sizes.

Outline Set Button
OriginalOutline button variants across colours and sizes with a hover fill.

Ghost Set Button
OriginalGhost and text buttons with subtle hover backgrounds.

Soft Set Button
OriginalSoft, tinted buttons across semantic colours.

Gradient Set Button
OriginalGradient-filled buttons with a hover shift.
Icon Leading Button
OriginalButtons with leading and trailing icons.
Icon Only Button
OriginalSquare and circular icon-only buttons with tooltips and aria-labels.

