Neumorph Button
Original · freeSoft neumorphic buttons on a matching surface.
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-neumorph.json"use client";
import { useState, type ReactNode } from "react";
type Size = "sm" | "md" | "lg";
const sizeMap: Record<Size, string> = {
sm: "h-9 px-4 text-sm rounded-xl",
md: "h-11 px-5 text-sm rounded-2xl",
lg: "h-14 px-7 text-base rounded-2xl",
};
const raised =
"shadow-[6px_6px_12px_rgba(148,163,184,0.55),-6px_-6px_12px_rgba(255,255,255,0.95)] " +
"dark:shadow-[6px_6px_14px_rgba(2,6,23,0.75),-5px_-5px_12px_rgba(71,85,105,0.45)]";
const pressed =
"shadow-[inset_5px_5px_10px_rgba(148,163,184,0.6),inset_-5px_-5px_10px_rgba(255,255,255,0.95)] " +
"dark:shadow-[inset_5px_5px_10px_rgba(2,6,23,0.8),inset_-4px_-4px_10px_rgba(71,85,105,0.45)]";
const activePress =
"active:scale-[0.98] " +
"active:shadow-[inset_5px_5px_10px_rgba(148,163,184,0.6),inset_-5px_-5px_10px_rgba(255,255,255,0.95)] " +
"dark:active:shadow-[inset_5px_5px_10px_rgba(2,6,23,0.8),inset_-4px_-4px_10px_rgba(71,85,105,0.45)]";
const base =
"relative inline-flex select-none items-center justify-center gap-2 font-semibold " +
"text-slate-600 dark:text-slate-200 bg-slate-200 dark:bg-slate-800 " +
"transition-all duration-150 ease-out outline-none " +
"hover:text-slate-900 dark:hover:text-white " +
activePress +
" focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 " +
"focus-visible:ring-offset-slate-200 dark:focus-visible:ring-offset-slate-800 " +
"disabled:cursor-not-allowed disabled:opacity-50 disabled:active:scale-100";
function cx(...parts: Array<string | false | null | undefined>): string {
return parts.filter(Boolean).join(" ");
}
export default function NeumorphButtons() {
const [liked, setLiked] = useState(false);
const [likes, setLikes] = useState(248);
const [muted, setMuted] = useState(false);
const [playing, setPlaying] = useState(false);
const [copied, setCopied] = useState(false);
const [loading, setLoading] = useState(false);
const [view, setView] = useState<"grid" | "list" | "board">("grid");
function toggleLike(): void {
setLiked((prev) => !prev);
setLikes((n) => n + (liked ? -1 : 1));
}
async function copyCode(): Promise<void> {
try {
await navigator.clipboard.writeText("npm i @webinnoventix/neumorph");
setCopied(true);
window.setTimeout(() => setCopied(false), 1600);
} catch {
setCopied(false);
}
}
function fakeSubmit(): void {
if (loading) return;
setLoading(true);
window.setTimeout(() => setLoading(false), 1900);
}
return (
<section className="relative w-full bg-slate-200 dark:bg-slate-900 px-6 py-20 sm:py-24">
<style>{`
@keyframes neumorph-spin { to { transform: rotate(360deg); } }
@keyframes neumorph-pop {
0% { transform: scale(1); }
40% { transform: scale(1.35); }
70% { transform: scale(0.9); }
100% { transform: scale(1); }
}
@keyframes neumorph-check {
from { opacity: 0; transform: translateY(3px) scale(0.9); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
.neumorph-spin { animation: neumorph-spin 0.8s linear infinite; }
.neumorph-pop { animation: neumorph-pop 0.4s ease-out; }
.neumorph-check { animation: neumorph-check 0.2s ease-out; }
@media (prefers-reduced-motion: reduce) {
.neumorph-spin, .neumorph-pop, .neumorph-check { animation: none !important; }
}
`}</style>
<div className="mx-auto max-w-4xl">
<header className="mb-14 text-center">
<p className="mb-3 text-xs font-semibold uppercase tracking-[0.2em] text-indigo-500 dark:text-indigo-400">
FreeCode / Buttons
</p>
<h2 className="text-3xl font-bold tracking-tight text-slate-800 dark:text-slate-100 sm:text-4xl">
Neumorphic buttons
</h2>
<p className="mx-auto mt-3 max-w-md text-sm text-slate-500 dark:text-slate-400">
Soft, tactile controls that share the surface they sit on and press
inward when you touch them.
</p>
</header>
<div className="grid gap-8">
{/* Sizes + variants */}
<Panel title="Sizes & emphasis">
<div className="flex flex-wrap items-center gap-5">
<button type="button" className={cx(base, raised, sizeMap.sm)}>
Small
</button>
<button type="button" className={cx(base, raised, sizeMap.md)}>
Medium
</button>
<button type="button" className={cx(base, raised, sizeMap.lg)}>
Large
</button>
<button
type="button"
className={cx(
base,
raised,
sizeMap.md,
"text-indigo-600 dark:text-indigo-300 hover:text-indigo-700 dark:hover:text-indigo-200",
)}
>
<SparkIcon />
Get started
</button>
<button type="button" disabled className={cx(base, raised, sizeMap.md)}>
Disabled
</button>
</div>
</Panel>
{/* Async + copy */}
<Panel title="Loading & clipboard">
<div className="flex flex-wrap items-center gap-5">
<button
type="button"
onClick={fakeSubmit}
aria-busy={loading}
disabled={loading}
className={cx(base, raised, sizeMap.md, "min-w-[9.5rem]")}
>
{loading ? (
<>
<span
className="neumorph-spin h-4 w-4 rounded-full border-2 border-slate-400/40 border-t-indigo-500"
aria-hidden="true"
/>
Saving…
</>
) : (
<>
<CloudIcon />
Save changes
</>
)}
</button>
<button
type="button"
onClick={copyCode}
className={cx(base, raised, sizeMap.md, "min-w-[11rem] font-mono text-[13px]")}
>
{copied ? (
<span className="neumorph-check flex items-center gap-2 text-emerald-600 dark:text-emerald-400">
<CheckIcon />
Copied!
</span>
) : (
<>
<CopyIcon />
npm i neumorph
</>
)}
</button>
</div>
</Panel>
{/* Icon toggles */}
<Panel title="Icon toggles">
<div className="flex flex-wrap items-center gap-5">
<button
type="button"
onClick={() => setPlaying((p) => !p)}
aria-pressed={playing}
aria-label={playing ? "Pause" : "Play"}
className={cx(base, playing ? pressed : raised, "h-14 w-14 rounded-full p-0")}
>
{playing ? <PauseIcon /> : <PlayIcon />}
</button>
<button
type="button"
onClick={() => setMuted((m) => !m)}
aria-pressed={muted}
aria-label={muted ? "Unmute" : "Mute"}
className={cx(base, muted ? pressed : raised, "h-14 w-14 rounded-full p-0")}
>
{muted ? <MuteIcon /> : <SoundIcon />}
</button>
<button
type="button"
onClick={toggleLike}
aria-pressed={liked}
className={cx(
base,
raised,
"h-14 gap-2.5 rounded-full px-6",
liked && "text-rose-500 dark:text-rose-400 hover:text-rose-600",
)}
>
<span className={liked ? "neumorph-pop" : undefined}>
<HeartIcon filled={liked} />
</span>
<span className="tabular-nums">{likes}</span>
</button>
</div>
</Panel>
{/* Segmented control */}
<Panel title="Segmented control">
<div
role="group"
aria-label="View mode"
className={cx(
"inline-flex gap-1 rounded-2xl bg-slate-200 p-1.5 dark:bg-slate-800",
pressed,
)}
>
{(["grid", "list", "board"] as const).map((mode) => {
const active = view === mode;
return (
<button
key={mode}
type="button"
onClick={() => setView(mode)}
aria-pressed={active}
className={cx(
"inline-flex h-9 items-center gap-2 rounded-xl px-4 text-sm font-semibold capitalize outline-none transition-all duration-150",
"focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-200 dark:focus-visible:ring-offset-slate-800",
active
? cx(
"bg-slate-200 text-indigo-600 dark:bg-slate-800 dark:text-indigo-300",
raised,
)
: "text-slate-500 hover:text-slate-800 dark:text-slate-400 dark:hover:text-slate-100",
)}
>
{mode}
</button>
);
})}
</div>
</Panel>
{/* Social */}
<Panel title="Continue with">
<div className="flex flex-wrap items-center gap-5">
<SocialButton label="Continue with GitHub">
<GithubGlyph />
GitHub
</SocialButton>
<SocialButton label="Continue with Google">
<GoogleGlyph />
Google
</SocialButton>
<SocialButton label="Continue with Apple">
<AppleGlyph />
Apple
</SocialButton>
<button
type="button"
aria-label="Continue with X"
className={cx(base, raised, "h-12 w-12 rounded-full p-0")}
>
<XGlyph />
</button>
</div>
</Panel>
</div>
</div>
</section>
);
}
function Panel({ title, children }: { title: string; children: ReactNode }) {
return (
<div className="rounded-3xl bg-slate-200 p-7 shadow-[inset_2px_2px_6px_rgba(148,163,184,0.35),inset_-2px_-2px_6px_rgba(255,255,255,0.7)] dark:bg-slate-800/60 dark:shadow-[inset_2px_2px_6px_rgba(2,6,23,0.6),inset_-2px_-2px_6px_rgba(71,85,105,0.35)]">
<h3 className="mb-6 text-xs font-semibold uppercase tracking-[0.15em] text-slate-400 dark:text-slate-500">
{title}
</h3>
{children}
</div>
);
}
function SocialButton({
label,
children,
}: {
label: string;
children: ReactNode;
}) {
return (
<button type="button" aria-label={label} className={cx(base, raised, sizeMap.md)}>
{children}
</button>
);
}
/* Icons -------------------------------------------------------------- */
function SparkIcon() {
return (
<svg width="16" height="16" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M12 3v4M12 17v4M5 12H3M21 12h-2M6 6l1.5 1.5M16.5 16.5L18 18M18 6l-1.5 1.5M7.5 16.5L6 18"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
<circle cx="12" cy="12" r="3" stroke="currentColor" strokeWidth="2" />
</svg>
);
}
function CloudIcon() {
return (
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M7 18a4 4 0 0 1 0-8 5 5 0 0 1 9.6-1.3A3.5 3.5 0 0 1 17 18H7Z"
stroke="currentColor"
strokeWidth="2"
strokeLinejoin="round"
/>
</svg>
);
}
function CopyIcon() {
return (
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<rect x="9" y="9" width="11" height="11" rx="2" stroke="currentColor" strokeWidth="2" />
<path
d="M5 15V5a2 2 0 0 1 2-2h8"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
);
}
function CheckIcon() {
return (
<svg width="15" height="15" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M20 6 9 17l-5-5"
stroke="currentColor"
strokeWidth="2.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}
function PlayIcon() {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M8 5.5v13a1 1 0 0 0 1.5.87l11-6.5a1 1 0 0 0 0-1.74l-11-6.5A1 1 0 0 0 8 5.5Z" />
</svg>
);
}
function PauseIcon() {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<rect x="6" y="5" width="4" height="14" rx="1.5" />
<rect x="14" y="5" width="4" height="14" rx="1.5" />
</svg>
);
}
function SoundIcon() {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M4 9v6h4l5 4V5L8 9H4Z"
stroke="currentColor"
strokeWidth="2"
strokeLinejoin="round"
/>
<path
d="M16 8.5a5 5 0 0 1 0 7M18.5 6a8.5 8.5 0 0 1 0 12"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
);
}
function MuteIcon() {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M4 9v6h4l5 4V5L8 9H4Z"
stroke="currentColor"
strokeWidth="2"
strokeLinejoin="round"
/>
<path
d="m16 9 5 6M21 9l-5 6"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
/>
</svg>
);
}
function HeartIcon({ filled }: { filled: boolean }) {
return (
<svg
width="19"
height="19"
viewBox="0 0 24 24"
fill={filled ? "currentColor" : "none"}
aria-hidden="true"
>
<path
d="M12 20.5 4.2 12.7a4.8 4.8 0 0 1 6.8-6.8l1 1 1-1a4.8 4.8 0 1 1 6.8 6.8L12 20.5Z"
stroke="currentColor"
strokeWidth="2"
strokeLinejoin="round"
/>
</svg>
);
}
/* Brand-style glyphs (simplified, not official logos) ---------------- */
function GithubGlyph() {
return (
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M12 2a10 10 0 0 0-3.16 19.49c.5.09.68-.22.68-.48v-1.7c-2.78.6-3.37-1.34-3.37-1.34-.45-1.16-1.11-1.47-1.11-1.47-.91-.62.07-.6.07-.6 1 .07 1.53 1.03 1.53 1.03.89 1.53 2.34 1.09 2.91.83.09-.65.35-1.09.63-1.34-2.22-.25-4.55-1.11-4.55-4.94 0-1.09.39-1.98 1.03-2.68-.1-.25-.45-1.27.1-2.65 0 0 .84-.27 2.75 1.02a9.5 9.5 0 0 1 5 0c1.91-1.29 2.75-1.02 2.75-1.02.55 1.38.2 2.4.1 2.65.64.7 1.03 1.59 1.03 2.68 0 3.84-2.34 4.68-4.57 4.93.36.31.68.92.68 1.85v2.74c0 .27.18.58.69.48A10 10 0 0 0 12 2Z" />
</svg>
);
}
function GoogleGlyph() {
return (
<svg width="18" height="18" viewBox="0 0 24 24" aria-hidden="true">
<path
fill="#EA4335"
d="M12 10.2v3.9h5.5a4.7 4.7 0 0 1-2 3.1v2.6h3.3c1.9-1.8 3-4.4 3-7.5 0-.7-.06-1.4-.18-2.1H12Z"
/>
<path
fill="#34A853"
d="M12 22c2.7 0 5-.9 6.6-2.4l-3.3-2.6c-.9.6-2 1-3.3 1a5.8 5.8 0 0 1-5.4-4H3.2v2.6A10 10 0 0 0 12 22Z"
/>
<path
fill="#FBBC05"
d="M6.6 14a5.9 5.9 0 0 1 0-3.8V7.6H3.2a10 10 0 0 0 0 8.9L6.6 14Z"
/>
<path
fill="#4285F4"
d="M12 6.2c1.5 0 2.8.5 3.8 1.5l2.9-2.9A10 10 0 0 0 3.2 7.6l3.4 2.6A5.8 5.8 0 0 1 12 6.2Z"
/>
</svg>
);
}
function AppleGlyph() {
return (
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M16 12.5c0-2.3 1.9-3.4 2-3.5-1.1-1.6-2.8-1.8-3.4-1.8-1.4-.15-2.8.85-3.5.85-.7 0-1.85-.83-3-.8-1.6.02-3 .93-3.8 2.36-1.6 2.8-.4 6.95 1.15 9.22.76 1.11 1.67 2.36 2.86 2.31 1.15-.05 1.58-.74 2.97-.74 1.38 0 1.77.74 2.98.72 1.23-.02 2.01-1.13 2.76-2.25.87-1.29 1.23-2.54 1.25-2.6-.03-.02-2.4-.92-2.43-3.66ZM13.7 5.6c.63-.77 1.06-1.83.94-2.9-.91.04-2.01.61-2.67 1.37-.59.68-1.1 1.76-.96 2.8 1.01.08 2.05-.51 2.69-1.27Z" />
</svg>
);
}
function XGlyph() {
return (
<svg width="17" height="17" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
<path d="M17.5 3h3l-7 8 8.2 10h-6.4l-5-6.1L4.6 21H1.5l7.5-8.6L1.2 3h6.6l4.5 5.6L17.5 3Zm-1.1 16h1.7L7.7 4.8H5.9L16.4 19Z" />
</svg>
);
}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.

