Web InnoventixFreeCode

3D Press Button

Original · free

Chunky 3D buttons that depress on press.

byWeb InnoventixReact + Tailwind
btn3dpressbuttons
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-3d-press.json
btn-3d-press.tsx
"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 &amp; 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 &amp; 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

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 →