3D Button Hover Effect
Original · freeA chunky 3D push-button that depresses on hover and 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/hover-3d-button.json"use client";
import { useState, type ReactNode } from "react";
type Variant = "indigo" | "emerald" | "rose" | "amber" | "sky" | "slate";
const VARIANTS = {
indigo: {
edge: "bg-gradient-to-r from-indigo-900 via-indigo-800 to-indigo-900 dark:from-black dark:via-indigo-950 dark:to-black",
front: "bg-indigo-500 text-white dark:bg-indigo-500",
shadow: "bg-indigo-950/45 dark:bg-black/60",
ring: "focus-visible:outline-indigo-400",
},
emerald: {
edge: "bg-gradient-to-r from-emerald-900 via-emerald-800 to-emerald-900 dark:from-black dark:via-emerald-950 dark:to-black",
front: "bg-emerald-500 text-emerald-950 dark:bg-emerald-500 dark:text-emerald-950",
shadow: "bg-emerald-950/45 dark:bg-black/60",
ring: "focus-visible:outline-emerald-400",
},
rose: {
edge: "bg-gradient-to-r from-rose-900 via-rose-800 to-rose-900 dark:from-black dark:via-rose-950 dark:to-black",
front: "bg-rose-500 text-white dark:bg-rose-500",
shadow: "bg-rose-950/45 dark:bg-black/60",
ring: "focus-visible:outline-rose-400",
},
amber: {
edge: "bg-gradient-to-r from-amber-800 via-amber-700 to-amber-800 dark:from-black dark:via-amber-950 dark:to-black",
front: "bg-amber-400 text-amber-950 dark:bg-amber-400 dark:text-amber-950",
shadow: "bg-amber-900/45 dark:bg-black/60",
ring: "focus-visible:outline-amber-300",
},
sky: {
edge: "bg-gradient-to-r from-sky-900 via-sky-800 to-sky-900 dark:from-black dark:via-sky-950 dark:to-black",
front: "bg-sky-500 text-white dark:bg-sky-500",
shadow: "bg-sky-950/45 dark:bg-black/60",
ring: "focus-visible:outline-sky-400",
},
slate: {
edge: "bg-gradient-to-r from-slate-800 via-slate-700 to-slate-800 dark:from-black dark:via-slate-800 dark:to-black",
front: "bg-slate-200 text-slate-900 dark:bg-slate-700 dark:text-slate-50",
shadow: "bg-slate-900/40 dark:bg-black/60",
ring: "focus-visible:outline-slate-400",
},
} satisfies Record<
Variant,
{ edge: string; front: string; shadow: string; ring: string }
>;
type Push3DButtonProps = {
children: ReactNode;
variant?: Variant;
type?: "button" | "submit";
onClick?: () => void;
ariaLabel?: string;
className?: string;
};
function Push3DButton({
children,
variant = "indigo",
type = "button",
onClick,
ariaLabel,
className = "",
}: Push3DButtonProps) {
const v = VARIANTS[variant];
return (
<button
type={type}
onClick={onClick}
aria-label={ariaLabel}
className={[
"group relative inline-flex cursor-pointer select-none appearance-none",
"rounded-2xl border-0 bg-transparent p-0 outline-none",
"focus-visible:outline focus-visible:outline-2 focus-visible:outline-offset-[6px]",
v.ring,
className,
].join(" ")}
>
{/* soft cast shadow that shrinks as the button is pushed down */}
<span
aria-hidden="true"
className={[
"absolute inset-0 rounded-2xl blur-[2px] will-change-transform",
"translate-y-[3px] opacity-70 transition-transform duration-150 ease-out",
"group-hover:translate-y-[1px] group-active:translate-y-0",
"motion-reduce:transition-none",
v.shadow,
].join(" ")}
/>
{/* the fixed base / side wall — this is the depth you sink into */}
<span
aria-hidden="true"
className={["absolute inset-0 rounded-2xl", v.edge].join(" ")}
/>
{/* the top face — lifts 6px at rest, sinks to 1px on hover/press */}
<span
className={[
"relative flex items-center justify-center gap-2 rounded-2xl",
"px-7 py-3.5 text-sm font-semibold tracking-tight",
"shadow-[inset_0_1px_0_rgba(255,255,255,0.35),inset_0_-1px_0_rgba(0,0,0,0.12)]",
"-translate-y-[6px] transition-transform duration-150 ease-out will-change-transform",
"group-hover:-translate-y-[3px] group-active:-translate-y-[1px]",
"motion-reduce:transition-none motion-reduce:group-hover:-translate-y-[6px]",
v.front,
].join(" ")}
>
{children}
</span>
</button>
);
}
export default function Hover3DButton() {
const [launched, setLaunched] = useState(false);
const [cart, setCart] = useState(0);
const [liked, setLiked] = useState(false);
const [likes, setLikes] = useState(248);
const [copied, setCopied] = useState(false);
function handleCopy() {
setCopied(true);
if (typeof navigator !== "undefined" && navigator.clipboard) {
void navigator.clipboard.writeText("npm i motion").catch(() => {});
}
window.setTimeout(() => setCopied(false), 1600);
}
function toggleLike() {
setLiked((prev) => {
setLikes((n) => (prev ? n - 1 : n + 1));
return !prev;
});
}
return (
<section className="relative w-full overflow-hidden bg-slate-50 px-6 py-20 text-slate-900 dark:bg-slate-950 dark:text-slate-100 sm:py-28">
<style>{`
@keyframes h3d-nudge {
0%, 100% { transform: translateX(0); }
50% { transform: translateX(3px); }
}
.h3d-arrow { animation: h3d-nudge 1.6s ease-in-out infinite; }
@media (prefers-reduced-motion: reduce) {
.h3d-arrow { animation: none; }
}
`}</style>
{/* decorative grid backdrop */}
<div
aria-hidden="true"
className="pointer-events-none absolute inset-0 [background-image:linear-gradient(to_right,rgba(100,116,139,0.08)_1px,transparent_1px),linear-gradient(to_bottom,rgba(100,116,139,0.08)_1px,transparent_1px)] [background-size:44px_44px]"
/>
<div className="relative mx-auto max-w-4xl">
<div className="mx-auto max-w-2xl 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-medium text-slate-600 dark:border-slate-800 dark:bg-slate-900 dark:text-slate-400">
<span className="h-1.5 w-1.5 rounded-full bg-emerald-500" />
Tactile controls
</span>
<h2 className="mt-5 text-balance text-3xl font-bold tracking-tight sm:text-4xl">
Buttons that push back
</h2>
<p className="mx-auto mt-3 max-w-md text-pretty text-sm leading-relaxed text-slate-600 dark:text-slate-400">
A chunky 3D face that depresses into its base on hover and press —
fully keyboard operable, with real actions wired to every click.
</p>
</div>
{/* hero button */}
<div className="mt-10 flex justify-center">
<Push3DButton
variant="indigo"
onClick={() => setLaunched((p) => !p)}
ariaLabel={launched ? "Reset onboarding" : "Get started for free"}
className="text-base"
>
<span className="px-1 text-[0.95rem]">
{launched ? "You're all set" : "Get started free"}
</span>
{launched ? (
<svg
viewBox="0 0 20 20"
fill="none"
aria-hidden="true"
className="h-4 w-4"
>
<path
d="M4 10.5l3.5 3.5L16 6"
stroke="currentColor"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
) : (
<svg
viewBox="0 0 20 20"
fill="none"
aria-hidden="true"
className="h3d-arrow h-4 w-4"
>
<path
d="M4 10h11M11 5.5L15.5 10 11 14.5"
stroke="currentColor"
strokeWidth="2.2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
)}
</Push3DButton>
</div>
{/* action grid */}
<div className="mt-14 grid gap-4 sm:grid-cols-3">
{/* add to cart */}
<div className="flex flex-col items-center gap-4 rounded-2xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900">
<Push3DButton
variant="emerald"
onClick={() => setCart((n) => n + 1)}
ariaLabel="Add one item to cart"
>
<svg
viewBox="0 0 20 20"
fill="none"
aria-hidden="true"
className="h-4 w-4"
>
<path
d="M3 3h2l1.6 9.3a1 1 0 0 0 1 .7h6.8a1 1 0 0 0 1-.8L17 6H6"
stroke="currentColor"
strokeWidth="1.8"
strokeLinecap="round"
strokeLinejoin="round"
/>
<circle cx="8" cy="17" r="1.3" fill="currentColor" />
<circle cx="15" cy="17" r="1.3" fill="currentColor" />
</svg>
Add to cart
</Push3DButton>
<p
className="text-xs text-slate-500 dark:text-slate-400"
aria-live="polite"
>
{cart === 0
? "Cart is empty"
: `${cart} item${cart === 1 ? "" : "s"} in cart`}
</p>
</div>
{/* like */}
<div className="flex flex-col items-center gap-4 rounded-2xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900">
<Push3DButton
variant="rose"
onClick={toggleLike}
ariaLabel={liked ? "Remove like" : "Like this"}
>
<svg
viewBox="0 0 20 20"
fill={liked ? "currentColor" : "none"}
aria-hidden="true"
className="h-4 w-4"
>
<path
d="M10 16.5S3.5 12.4 3.5 7.9A3.4 3.4 0 0 1 10 6.3a3.4 3.4 0 0 1 6.5 1.6c0 4.5-6.5 8.6-6.5 8.6Z"
stroke="currentColor"
strokeWidth="1.8"
strokeLinejoin="round"
/>
</svg>
{liked ? "Liked" : "Like"}
</Push3DButton>
<p
className="text-xs text-slate-500 dark:text-slate-400"
aria-live="polite"
>
{likes.toLocaleString()} people like this
</p>
</div>
{/* copy */}
<div className="flex flex-col items-center gap-4 rounded-2xl border border-slate-200 bg-white p-6 dark:border-slate-800 dark:bg-slate-900">
<Push3DButton
variant="slate"
onClick={handleCopy}
ariaLabel="Copy install command"
>
{copied ? (
<svg
viewBox="0 0 20 20"
fill="none"
aria-hidden="true"
className="h-4 w-4"
>
<path
d="M4 10.5l3.5 3.5L16 6"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
) : (
<svg
viewBox="0 0 20 20"
fill="none"
aria-hidden="true"
className="h-4 w-4"
>
<rect
x="7"
y="7"
width="9"
height="9"
rx="1.6"
stroke="currentColor"
strokeWidth="1.7"
/>
<path
d="M4 13V5a1 1 0 0 1 1-1h8"
stroke="currentColor"
strokeWidth="1.7"
strokeLinecap="round"
/>
</svg>
)}
{copied ? "Copied!" : "npm i motion"}
</Push3DButton>
<p className="text-xs text-slate-500 dark:text-slate-400">
Click to copy to clipboard
</p>
</div>
</div>
<p className="mt-10 text-center text-xs text-slate-400 dark:text-slate-500">
Tip: focus a button with{" "}
<kbd className="rounded border border-slate-300 bg-slate-100 px-1.5 py-0.5 font-mono text-[0.7rem] text-slate-600 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300">
Tab
</kbd>{" "}
and press{" "}
<kbd className="rounded border border-slate-300 bg-slate-100 px-1.5 py-0.5 font-mono text-[0.7rem] text-slate-600 dark:border-slate-700 dark:bg-slate-800 dark:text-slate-300">
Space
</kbd>{" "}
to feel the push.
</p>
</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 →
Card Lift Hover Effect
OriginalA card that lifts with a growing shadow on hover.

Card Glow Hover Effect
OriginalA card with a coloured glow that blooms on hover.

Card Border Draw Hover Effect
OriginalA card whose border draws itself in on hover.

Card Gradient Shift Hover Effect
OriginalA card whose gradient background shifts on hover.

Card Tilt Hover Effect
OriginalA card that tilts in 3D toward the cursor on hover.

Card Spotlight Hover Effect
OriginalA card with a cursor-following radial spotlight.

Link Underline Hover Effect
OriginalAnimated link underline effects: grow, slide and centre-out.

Link Slide Hover Effect
OriginalLinks whose label slides up to a duplicate on hover.

Button Fill Hover Effect
OriginalButtons with a colour fill that sweeps across on hover.

Button Shine Hover Effect
OriginalButtons with a diagonal shine sweep on hover.
Icon Fill Hover Effect
OriginalIcon buttons whose icon and background fill on hover.
Icon Bounce Hover Effect
OriginalIcons that bounce or spin playfully on hover.

