# Depth Button (`depth-button`)

> A tactile raised button with a fixed depth base — lifts on hover, snaps down on press.

- **Docs:** https://www.mellowui.com/components/depth-button
- **Markdown:** https://www.mellowui.com/components/depth-button.md
- **Registry:** https://www.mellowui.com/r/depth-button.json
- **Tool prompt:** https://www.mellowui.com/api/prompt/depth-button
- **Categories:** button, interactive, 3d
- **Dependencies:** none

## AI prompt

Add a DepthButton from the mellow library. It's a raised 3D button with a depth base that the face snaps into on press. Use variant='default' for neutral glass style, variant='accent' for purple, or variant='destructive' for red delete/danger actions. The depth prop (default 4) controls how tall the raised block looks in pixels.

## Install

```bash
npx shadcn@latest add https://mellowui.com/r/depth-button.json
```

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `children` | `React.ReactNode` | — | Button label content. |
| `variant` | `"default" \| "accent" \| "destructive"` | `"default"` | Color scheme — default is neutral glass, accent is purple, destructive is red for delete/danger actions. |
| `depth` | `number` | `4` | Height of the depth base in pixels. |
| `className` | `string` | — | Additional CSS classes. |

## Design notes

- Client component (`"use client"`) — required for animation and refs.
- Uses Mellow design tokens: `--ink`, `--ink-rgb`, `--background`, `--background-rgb`, `--rule`, `--font-sans`, `--font-serif`, `--font-mono`. Tokens auto-flip between light and dark themes — never hardcode colors.
- Respects `prefers-reduced-motion: reduce` where applicable.
- Self-contained — no shared utilities. Copy-paste, not a package.

## Source

```tsx
"use client";

import React, { useState, useRef, type ButtonHTMLAttributes } from "react";

export interface DepthButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
  children: React.ReactNode;
  variant?: "default" | "accent" | "destructive";
  depth?: number;
  size?: "default" | "icon";
  sound?: boolean;
  onTouchStart?: React.TouchEventHandler<HTMLButtonElement>;
  onTouchEnd?: React.TouchEventHandler<HTMLButtonElement>;
  onTouchCancel?: React.TouchEventHandler<HTMLButtonElement>;
}

export function DepthButton({
  children,
  variant = "default",
  depth = 4,
  size = "default",
  sound = true,
  className,
  style,
  disabled,
  onMouseDown,
  onMouseUp,
  onMouseEnter,
  onMouseLeave,
  onTouchStart,
  onTouchEnd,
  onTouchCancel,
  ...props
}: DepthButtonProps) {
  const [pressed, setPressed] = useState(false);
  const [hovered, setHovered] = useState(false);
  const audioCtxRef = useRef<AudioContext | null>(null);
  const releaseSoundArmedRef = useRef(false);

  function playMechanicalPhase(phase: "press" | "release") {
    if (!sound) return;
    try {
      const AudioCtx =
        window.AudioContext ||
        (window as unknown as { webkitAudioContext: typeof AudioContext }).webkitAudioContext;
      if (!audioCtxRef.current || audioCtxRef.current.state === "closed") {
        audioCtxRef.current = new AudioCtx();
      }
      const ctx = audioCtxRef.current;

      const schedule = () => {
        const sampleRate = ctx.sampleRate;
        const duration = phase === "press" ? 0.04 : 0.026;
        const decayExp = phase === "press" ? 7 : 11;
        const noiseAmp = phase === "press" ? 0.72 : 0.48;
        const lpHz = phase === "press" ? 2800 : 5500;
        const gainLinear = phase === "press" ? 0.56 : 0.4;

        const buf = ctx.createBuffer(1, Math.floor(sampleRate * duration), sampleRate);
        const data = buf.getChannelData(0);
        for (let i = 0; i < data.length; i++) {
          const t = i / data.length;
          data[i] = (Math.random() * 2 - 1) * Math.pow(1 - t, decayExp) * noiseAmp;
        }

        const src = ctx.createBufferSource();
        src.buffer = buf;

        const lp = ctx.createBiquadFilter();
        lp.type = "lowpass";
        lp.frequency.value = lpHz;

        const gain = ctx.createGain();
        gain.gain.value = gainLinear;

        src.connect(lp);
        lp.connect(gain);
        gain.connect(ctx.destination);
        src.start();
      };

      if (ctx.state === "suspended") {
        ctx.resume().then(schedule);
      } else {
        schedule();
      }
    } catch {
    }
  }

  const isAccent = variant === "accent";
  const isDestructive = variant === "destructive";
  const isIcon = size === "icon";

  const faceTransform = pressed
    ? `translateY(${depth}px)`
    : hovered
    ? "translateY(-2px)"
    : "translateY(0px)";

  const faceTransition = pressed
    ? "transform 55ms ease, box-shadow 55ms ease"
    : "transform 300ms cubic-bezier(0.34, 1.4, 0.64, 1), box-shadow 200ms ease";

  return (
    <button
      disabled={disabled}
      className={[
        "relative inline-flex bg-transparent border-0 p-0",
        disabled ? "cursor-not-allowed opacity-50" : "cursor-pointer",
        className,
      ].filter(Boolean).join(" ")}
      style={style}
      onMouseEnter={(e) => {
        if (!disabled) setHovered(true);
        onMouseEnter?.(e);
      }}
      onMouseLeave={(e) => {
        setHovered(false);
        setPressed(false);
        releaseSoundArmedRef.current = false;
        onMouseLeave?.(e);
      }}
      onMouseDown={(e) => {
        if (!disabled) {
          setPressed(true);
          playMechanicalPhase("press");
          releaseSoundArmedRef.current = true;
        }
        onMouseDown?.(e);
      }}
      onMouseUp={(e) => {
        if (!disabled && releaseSoundArmedRef.current) {
          playMechanicalPhase("release");
          releaseSoundArmedRef.current = false;
        }
        setPressed(false);
        onMouseUp?.(e);
      }}
      onTouchStart={(e) => {
        if (!disabled) {
          setPressed(true);
          playMechanicalPhase("press");
          releaseSoundArmedRef.current = true;
        }
        onTouchStart?.(e);
      }}
      onTouchEnd={(e) => {
        if (!disabled && releaseSoundArmedRef.current) {
          playMechanicalPhase("release");
          releaseSoundArmedRef.current = false;
        }
        setPressed(false);
        setHovered(false);
        onTouchEnd?.(e);
      }}
      onTouchCancel={(e) => {
        releaseSoundArmedRef.current = false;
        setPressed(false);
        setHovered(false);
        onTouchCancel?.(e);
      }}
      {...props}
    >
      <span
        aria-hidden="true"
        className={[
          "absolute inset-0 rounded-[6px] border",
          isAccent
            ? "bg-[oklch(0.28_0.18_250)] border-[oklch(0.4_0.2_250/0.5)]"
            : isDestructive
            ? "bg-[oklch(0.28_0.14_25)] border-[oklch(0.4_0.16_25/0.5)]"
            : "bg-[rgba(var(--ink-rgb),0.03)] border-[rgba(var(--ink-rgb),0.06)]",
        ].join(" ")}
        style={{ transform: `translateY(${depth}px)` }}
      />
      <span
        className={[
          isIcon
            ? "absolute inset-0 flex items-center justify-center rounded-[6px]"
            : "relative inline-flex items-center justify-center gap-2 px-5 py-[0.6rem] rounded-[6px]",
          "[font-family:var(--font-sans)] text-sm font-medium tracking-[-0.01em] select-none",
          isAccent
            ? "bg-[oklch(0.52_0.24_250)] border border-[oklch(0.62_0.22_250)] text-[oklch(0.95_0.05_250)]"
            : isDestructive
            ? "bg-[oklch(0.52_0.19_25)] border border-[oklch(0.62_0.18_25)] text-[oklch(0.95_0.03_25)]"
            : "bg-[rgba(var(--ink-rgb),0.07)] border border-[rgba(var(--ink-rgb),0.12)] text-[var(--ink)]",
        ].join(" ")}
        style={{
          transform: faceTransform,
          transition: faceTransition,
          boxShadow: pressed
            ? "none"
            : "inset 0 1px 0 rgba(255,255,255,0.1), inset 0 -1px 0 rgba(0,0,0,0.15)",
        }}
      >
        {children}
      </span>
    </button>
  );
}

export default DepthButton;

```

## Demo

```tsx
"use client";

import React from "react";
import { DepthButton } from "../mellow/depth-button";

export default function DepthButtonDemo() {
  return (
    <div className="flex flex-col items-center gap-6 p-8">
      <div className="flex flex-wrap items-center justify-center gap-4">
        <DepthButton>Get Started</DepthButton>
        <DepthButton>Browse Docs</DepthButton>
        <DepthButton variant="accent">Deploy Now</DepthButton>
        <DepthButton variant="destructive">Delete</DepthButton>
      </div>
      <div className="flex flex-wrap items-center justify-center gap-4">
        <DepthButton depth={6}>Deep Press</DepthButton>
        <DepthButton variant="accent" depth={2}>Shallow</DepthButton>
        <DepthButton disabled>Disabled</DepthButton>
      </div>
      <p className="text-[rgba(var(--ink-rgb),0.35)] text-[0.8125rem]">
        Hover to lift · click to press
      </p>
    </div>
  );
}

```
