Web InnoventixFreeCode

Neumorph Button

Original · free

Soft neumorphic buttons on a matching surface.

byWeb InnoventixReact + Tailwind
btnneumorphbuttons
Open

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
btn-neumorph.tsx
"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

None (React + Tailwind only).

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 quote

Similar components

Browse all →