Skip to main content
Back to discovery

Live preview recorded from Mellow UI; component and demo published by Mellow UI. Preview: platform recorded · rights authorized.

Depth Button

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

MSource LandMellow UI

Why it stands out

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

Prompt

You are given a task to integrate a React component into your codebase.
Please verify your project has the following setup:
- shadcn/ui project structure
- Tailwind CSS v4.0
- TypeScript
- motion/react v11

If any of these are missing, provide instructions on how to setup project via shadcn CLI, install Tailwind or TypeScript.

Determine the default path for components and styles. Mellow components live in `components/mellow/`. If that folder does not exist yet, create it.

Copy-paste these files:

File location: components/depth-button-demo.tsx

File content: "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-4 p-4 sm:gap-6 sm:p-8">
      <div className="flex flex-wrap items-center justify-center gap-3 sm: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>
  );
}



File location: components/mellow/depth-button.tsx

File content: "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;



Usage notes:
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 blue, or variant='destructive' for red delete/danger actions. The depth prop (default 4) controls how tall the raised block looks in pixels.

Discovery vocabulary

Related by governed terms

Continue comparing

More from Mellow UI