Tactile 3D Pill Button

Tactile 3D Pill Button

3D Tactile Spring

Hyper-realistic 3D tactile button with recessed cavity slot, spring tilt physics, specular bevels, and glowing status beacon.

Interactive Playground & Studio

Live Controls
tactile-pill-button.tsx
Hover or click to interact
Customizer
PRESETS & VARIATIONS

All Variants & Themes

Click to test interactive spring physics on every theme

Default Cyan Beaconvariant="default"

1:1 reference design with turquoise indicator dot.

UI Pirate Magmavariant="orange"

Signature magma orange pill cap with glowing amber beacon.

Dark Obsidianvariant="dark"

Stealth midnight pill with violet status indicator.

Cyberpunk Matrixvariant="cyberpunk"

Neon reactor green beacon with tactile recessed tray.

Minimal Cleanvariant="minimal"

Crisp clean tactile pill with sapphire status beacon.

Tilted State PreviewstateMode="tilted"

Fixed mechanical spring tilt position (-9.23° angle).

Features & Physics

Recessed cavity slot with 3D depth shadows
Interactive 60fps spring tilt and depress on click
Pulsing radiant status beacon dot
Light and dark mode tailored specular lighting

Code & Integration

Customized live with your active playground prop settings:

"use client";

import React, { useState } from "react";
import { motion } from "framer-motion";

export type TactileButtonVariant = "default" | "dark" | "orange" | "cyberpunk" | "minimal";
export type TactileButtonState = "interactive" | "resting" | "tilted";

export interface TactilePillButtonProps {
  label?: string;
  dotColor?: string;
  variant?: TactileButtonVariant;
  size?: "xs" | "sm" | "md" | "lg" | "xl";
  tiltAngle?: number;
  onClick?: () => void;
  className?: string;
}

export function TactilePillButton({
  label = "Get Started",
  dotColor = "#54EAD8",
  variant = "default",
  size = "md",
  tiltAngle = -9.23,
  onClick,
  className = "",
}: TactilePillButtonProps) {
  const [isHovered, setIsHovered] = useState(false);
  const [isPressed, setIsPressed] = useState(false);

  return (
    <div
      className={`relative inline-block select-none ${className}`}
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={() => {
        setIsHovered(false);
        setIsPressed(false);
      }}
    >
      {/* Recessed Cavity Base Slot */}
      <div className="absolute inset-0 rounded-[16px] bg-[#D4D4D8] shadow-[inset_0_2px_4px_rgba(0,0,0,0.3)]" />

      {/* Tactile Cap with Lift & Tilt */}
      <motion.button
        type="button"
        onClick={onClick}
        onMouseDown={() => setIsPressed(true)}
        onMouseUp={() => setIsPressed(false)}
        animate={{
          rotate: isHovered ? tiltAngle : 0,
          y: isPressed ? 2 : isHovered ? -14 : 0,
          x: isHovered ? -4 : 0,
        }}
        transition={{ type: "spring", stiffness: 450, damping: 25 }}
        className="relative flex items-center gap-3 px-6 py-3 rounded-[16px] bg-[#EAEAEA] border border-white text-gray-800 font-bold text-sm shadow-[0_8px_16px_rgba(0,0,0,0.15)] cursor-pointer focus:outline-none"
      >
        <span
          className="w-2.5 h-2.5 rounded-full"
          style={{
            backgroundColor: dotColor,
            boxShadow: `0 0 8px ${dotColor}`,
          }}
        />
        <span>{label}</span>
      </motion.button>
    </div>
  );
}

export default TactilePillButton;

Peer Dependencies

Requires Framer Motion and Tailwind CSS for spring physics and layout tokens:

npm install framer-motion clsx lucide-react tailwind-merge

API & Props Reference

PropTypeDefaultDescription
labelstring"Get Started"Primary button call-to-action text.
dotColorstring"#00E5BE"Hex color code for the radiant glowing beacon indicator.
variant"default" | "dark" | "orange" | "cyberpunk" | "minimal""default"Theme style variant matching light or dark surfaces.
onClick() => voidundefinedOptional click handler event.