shadimage
evilbuttons
ui3d-buttonsource: ok · hit

ThreeDButton

A minimal 3D button with a clean deck-style press and sliding click sheen.

npx shadcn add "https://evilbuttons.radiumcoders.com/r/3d-button.json"
free

Visual style

3dminimal

Tags auto-generated by rules on 9/24/2026 — empty means no evidence, not lack of support.

Files1 · source cached 10/8/2026, 2:13:02 PM

components/evil-buttons/3d-button.tsx
"use client";

import * as React from "react";
import { motion, type HTMLMotionProps } from "motion/react";

import { cn } from "@/lib/utils";

type ThreeDButtonProps = HTMLMotionProps<"button"> & {
  children?: React.ReactNode;
};

const pressTransition = {
  type: "spring",
  stiffness: 620,
  damping: 32,
  mass: 0.5,
} as const;

export function ThreeDButton({
  className,
  children = "Continue",
  ...props
}: ThreeDButtonProps) {
  return (
    <motion.button
      initial="rest"
      whileHover="hover"
      whileTap="tap"
      className={cn(
        "group relative isolate inline-flex min-w-38 cursor-pointer rounded-full pb-1.5 outline-none",
        "focus-visible:ring-2 focus-visible:ring-zinc-500 focus-visible:ring-offset-2",
        "dark:focus-visible:ring-white",
        "disabled:pointer-events-none disabled:opacity-50",
        className,
      )}
      {...props}
    >
      {/* Deck (bottom layer / shadow block) */}
      <motion.span
        aria-hidden
        variants={{
          rest: { y: 0 },
          hover: { y: 1 },
          tap: { y: -1 },
        }}
        transition={pressTransition}
        className="absolute inset-x-0 bottom-0 top-1.5 -z-10 rounded-full bg-zinc-950 shadow-[0_10px_18px_rgba(24,24,27,0.2)] dark:bg-white dark:shadow-[0_10px_18px_rgba(255,255,255,0.15)]"
      />
      {/* Face (top layer / button surface) */}
      <motion.span
        variants={{
          // Whole-pixel moves only: tilting or fractionally scaling the face makes
          // Chrome drop anti-aliasing on its edge, which shows up as a dashed rim.
          rest: { y: 0 },
          hover: { y: -2 },
          tap: { y: 6 },
        }}
        transition={pressTransition}
        className={cn(
          "relative z-10 inline-flex w-full items-center justify-center overflow-hidden rounded-full px-7 py-3",
          "bg-zinc-50 text-sm font-semibold text-zinc-950 dark:bg-zinc-900 dark:text-white",
          // The rim is an inset ring rather than a border so it stays smooth while the face moves.
          "shadow-[inset_0_0_0_1px_rgb(212_212_216),inset_0_2px_0_rgba(255,255,255,0.9),inset_0_-2px_0_rgba(24,24,27,0.06)]",
          "dark:shadow-[inset_0_0_0_1px_rgb(255_255_255),inset_0_2px_0_rgba(255,255,255,0.15),inset_0_-2px_0_rgba(0,0,0,0.3)]",
          "backface-hidden will-change-transform",
        )}
      >
        {/* Press spot (darkens on press) */}
        <motion.span
          aria-hidden
          variants={{
            rest: { opacity: 0, scaleX: 0.25 },
            hover: { opacity: 0.3, scaleX: 0.45 },
            tap: { opacity: 1, scaleX: 1 },
          }}
          transition={pressTransition}
          className="pointer-events-none absolute inset-y-1 left-1/2 w-10 -translate-x-1/2 rounded-full bg-zinc-950/10 dark:bg-white/20 blur-sm"
        />
        {/* Sheen / gloss stripe */}
        <motion.span
          aria-hidden
          variants={{
            // Parked fully off the left edge so no sliver of it shows at rest.
            rest: { opacity: 0, x: "-100%" },
            hover: { opacity: 0.75, x: "-35%" },
            tap: { opacity: 0.9, x: "82%" },
          }}
          transition={pressTransition}
          className="pointer-events-none absolute inset-y-0 left-0 w-1/2 bg-linear-to-r from-transparent via-white to-transparent dark:via-white/20"
        />
        {/* Label */}
        <motion.span
          variants={{
            rest: { y: 0 },
            hover: { y: -1 },
            tap: { y: 1 },
          }}
          transition={pressTransition}
          className="relative z-10"
        >
          {children}
        </motion.span>
      </motion.span>
    </motion.button>
  );
}

More from evilbuttons

Browse all components in this library →