"use client";
import { PressDepth } from "@/components/interior/press-depth";
export function PressDepthDemo() {
return (
<div className="flex justify-center">
<PressDepth>Press me</PressDepth>
</div>
);
}
Press Depth Demo
By Özer
Try the interaction, copy the React source files, or download the complete example.
About Press Depth Demo
Press Depth Demo is an animated React and Tailwind CSS button by Özer. Try the interaction, copy the React source, or download the example with creator credit.
- Effects and categories
- Hover effects, Animated
- Using this example
- Copy a TSX file or download the original source and standalone demo. Test keyboard focus, motion and contrast in your own project.
Usage, Tailwind and licensing questions
Read the source files
lib/demos/press-depth-demo.tsx
"use client";
import { PressDepth } from "@/components/interior/press-depth";
export function PressDepthDemo() {
return (
<div className="flex justify-center">
<PressDepth>Press me</PressDepth>
</div>
);
}
components/interior/press-depth.tsx
"use client";
import { useCallback, useEffect, useRef, useState } from "react";
import { motion, useReducedMotion } from "motion/react";
const PRESS = { type: "spring", stiffness: 520, damping: 34, mass: 0.45 } as const;
export type UsePressDepthOptions = {
disabled?: boolean;
onPressStart?: () => void;
onPressEnd?: () => void;
};
export type PressOrigin = { x: number; y: number };
export type UsePressDepthResult = {
pressed: boolean;
origin: PressOrigin | null;
ref: (node: HTMLElement | null) => void;
bind: {
onPointerDown: (event: React.PointerEvent) => void;
onKeyDown: (event: React.KeyboardEvent) => void;
onKeyUp: (event: React.KeyboardEvent) => void;
onBlur: () => void;
};
};
export function usePressDepth(
options: UsePressDepthOptions = {},
): UsePressDepthResult {
const { disabled = false, onPressStart, onPressEnd } = options;
const [pressed, setPressed] = useState(false);
const [tracking, setTracking] = useState(false);
const [origin, setOrigin] = useState<PressOrigin | null>(null);
const node = useRef<HTMLElement | null>(null);
const pointer = useRef<number | null>(null);
const down = useRef(false);
const began = useRef(onPressStart);
began.current = onPressStart;
const ended = useRef(onPressEnd);
ended.current = onPressEnd;
const setDown = useCallback((next: boolean) => {
if (down.current === next) return;
down.current = next;
setPressed(next);
if (next) began.current?.();
else ended.current?.();
}, []);
const stop = useCallback(() => {
pointer.current = null;
setTracking(false);
setOrigin(null);
setDown(false);
}, [setDown]);
useEffect(() => {
if (!tracking) return;
const contains = (event: PointerEvent) => {
const el = node.current;
if (!el) return false;
const r = el.getBoundingClientRect();
return (
event.clientX >= r.left &&
event.clientX <= r.right &&
event.clientY >= r.top &&
event.clientY <= r.bottom
);
};
const move = (event: PointerEvent) => {
if (event.pointerId !== pointer.current) return;
setDown(contains(event));
};
const lift = (event: PointerEvent) => {
if (event.pointerId !== pointer.current) return;
stop();
};
const bail = () => stop();
const hidden = () => {
if (document.hidden) stop();
};
window.addEventListener("pointermove", move);
window.addEventListener("pointerup", lift);
window.addEventListener("pointercancel", lift);
window.addEventListener("blur", bail);
document.addEventListener("visibilitychange", hidden);
return () => {
window.removeEventListener("pointermove", move);
window.removeEventListener("pointerup", lift);
window.removeEventListener("pointercancel", lift);
window.removeEventListener("blur", bail);
document.removeEventListener("visibilitychange", hidden);
};
}, [tracking, setDown, stop]);
useEffect(() => {
if (disabled) stop();
}, [disabled, stop]);
const ref = useCallback((next: HTMLElement | null) => {
node.current = next;
}, []);
const bind = {
onPointerDown: (event: React.PointerEvent) => {
if (disabled) return;
if (event.pointerType === "mouse" && event.button !== 0) return;
const r = event.currentTarget.getBoundingClientRect();
setOrigin({
x: Math.max(-1, Math.min(1, ((event.clientX - r.left) / r.width) * 2 - 1)),
y: Math.max(-1, Math.min(1, ((event.clientY - r.top) / r.height) * 2 - 1)),
});
pointer.current = event.pointerId;
setTracking(true);
setDown(true);
},
onKeyDown: (event: React.KeyboardEvent) => {
if (disabled || event.repeat) return;
if (event.key === " " || event.key === "Enter") setDown(true);
},
onKeyUp: (event: React.KeyboardEvent) => {
if (event.key === " " || event.key === "Enter" || event.key === "Escape") {
setDown(false);
}
},
onBlur: () => stop(),
};
return { pressed, origin, ref, bind };
}
export type PressDepthProps = {
children: React.ReactNode;
depth?: number;
tilt?: number;
disabled?: boolean;
type?: "button" | "submit" | "reset";
onClick?: React.MouseEventHandler<HTMLButtonElement>;
className?: string;
"aria-label"?: string;
};
export function PressDepth({
children,
depth = 4,
tilt = 7,
disabled = false,
type = "button",
onClick,
className = "",
"aria-label": ariaLabel,
}: PressDepthProps) {
const reduced = useReducedMotion();
const { pressed, origin, ref, bind } = usePressDepth({ disabled });
const lean = pressed && origin && !reduced ? origin : null;
return (
<button
ref={ref}
type={type}
disabled={disabled}
aria-label={ariaLabel}
data-pressed={pressed ? "" : undefined}
onClick={onClick}
style={{
paddingBottom: depth,
touchAction: "manipulation",
WebkitTapHighlightColor: "transparent",
}}
className="group relative inline-flex select-none rounded-[9px] align-middle outline-none disabled:opacity-50"
{...bind}
>
<span
aria-hidden
style={{ top: depth }}
className="absolute inset-x-0 bottom-0 rounded-[9px] bg-stone-300 dark:bg-white/25"
/>
<motion.span
initial={false}
animate={{
y: pressed ? depth : 0,
rotateX: lean ? -lean.y * tilt : 0,
rotateY: lean ? lean.x * tilt : 0,
}}
transition={reduced ? { duration: 0 } : PRESS}
style={{ transformPerspective: 340 }}
className={`relative inline-flex h-9 items-center justify-center gap-2 rounded-[9px] border border-stone-200 bg-white px-3.5 text-[13px] font-medium text-stone-700 group-focus-visible:ring-2 group-focus-visible:ring-stone-400 dark:border-white/[0.16] dark:bg-[#1D1D1A] dark:text-stone-200 dark:group-focus-visible:ring-stone-500 ${className}`}
>
<motion.span
aria-hidden
initial={false}
animate={{ opacity: pressed ? 0 : 1 }}
transition={reduced ? { duration: 0 } : PRESS}
className="pointer-events-none absolute inset-0 rounded-[9px] shadow-[inset_0_1.5px_0_rgba(255,255,255,0.95),inset_0_-1px_0_rgba(28,25,23,0.06)] dark:shadow-[inset_0_1.5px_0_rgba(255,255,255,0.09)]"
/>
{children}
</motion.span>
</button>
);
}
demo.tsx
export { PressDepthDemo as default } from "@/lib/demos/press-depth-demo";CSS
/*! tailwindcss v4.3.3 | MIT License | https://tailwindcss.com */
@layer properties;
@layer theme, base, components, utilities;
@layer theme {
:root, :host {
--color-stone-200: oklch(92.3% 0.003 48.717);
--color-stone-300: oklch(86.9% 0.005 56.366);
--color-stone-400: oklch(70.9% 0.01 56.259);
--color-stone-500: oklch(55.3% 0.013 58.071);
--color-stone-700: oklch(37.4% 0.01 67.558);
--color-white: #fff;
--spacing: 0.25rem;
--font-weight-medium: 500;
--default-transition-duration: 150ms;
--default-transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
--default-font-family: var(--font-geist-sans);
--default-mono-font-family: var(--font-geist-mono);
}
}
@layer base {
*, ::after, ::before, ::backdrop, ::file-selector-button {
box-sizing: border-box;
margin: 0;
padding: 0;
border: 0 solid;
}
html, :host {
line-height: 1.5;
-webkit-text-size-adjust: 100%;
tab-size: 4;
font-family: var(--default-font-family, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", "Noto Sans", Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");
font-feature-settings: var(--default-font-feature-settings, normal);
font-variation-settings: var(--default-font-variation-settings, normal);
-webkit-tap-highlight-color: transparent;
}
hr {
height: 0;
color: inherit;
border-top-width: 1px;
}
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
h1, h2, h3, h4, h5, h6 {
font-size: inherit;
font-weight: inherit;
}
a {
color: inherit;
-webkit-text-decoration: inherit;
text-decoration: inherit;
}
b, strong {
font-weight: bolder;
}
code, kbd, samp, pre {
font-family: var(--default-mono-font-family, ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);
font-feature-settings: var(--default-mono-font-feature-settings, normal);
font-variation-settings: var(--default-mono-font-variation-settings, normal);
font-size: 1em;
}
small {
font-size: 80%;
}
sub, sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
table {
text-indent: 0;
border-color: inherit;
border-collapse: collapse;
}
:-moz-focusring:where(:not(iframe)) {
outline: auto;
}
progress {
vertical-align: baseline;
}
summary {
display: list-item;
}
ol, ul, menu {
list-style: none;
}
img, svg, video, canvas, audio, iframe, embed, object {
display: block;
vertical-align: middle;
}
img, video {
max-width: 100%;
height: auto;
}
button, input, select, optgroup, textarea, ::file-selector-button {
font: inherit;
font-feature-settings: inherit;
font-variation-settings: inherit;
letter-spacing: inherit;
color: inherit;
border-radius: 0;
background-color: transparent;
opacity: 1;
}
:where(select:is([multiple], [size])) optgroup {
font-weight: bolder;
}
:where(select:is([multiple], [size])) optgroup option {
padding-inline-start: 20px;
}
::file-selector-button {
margin-inline-end: 4px;
}
::placeholder {
opacity: 1;
}
@supports (not (-webkit-appearance: -apple-pay-button)) or (contain-intrinsic-size: 1px) {
::placeholder {
color: currentcolor;
@supports (color: color-mix(in lab, red, red)) {
color: color-mix(in oklab, currentcolor 50%, transparent);
}
}
}
textarea {
resize: vertical;
}
::-webkit-search-decoration {
-webkit-appearance: none;
}
::-webkit-date-and-time-value {
min-height: 1lh;
text-align: inherit;
}
::-webkit-datetime-edit {
display: inline-flex;
}
::-webkit-datetime-edit-fields-wrapper {
padding: 0;
}
::-webkit-datetime-edit, ::-webkit-datetime-edit-year-field, ::-webkit-datetime-edit-month-field, ::-webkit-datetime-edit-day-field, ::-webkit-datetime-edit-hour-field, ::-webkit-datetime-edit-minute-field, ::-webkit-datetime-edit-second-field, ::-webkit-datetime-edit-millisecond-field, ::-webkit-datetime-edit-meridiem-field {
padding-block: 0;
}
::-webkit-calendar-picker-indicator {
line-height: 1;
}
:-moz-ui-invalid {
box-shadow: none;
}
button, input:where([type="button"], [type="reset"], [type="submit"]), ::file-selector-button {
appearance: button;
}
::-webkit-inner-spin-button, ::-webkit-outer-spin-button {
height: auto;
}
[hidden]:where(:not([hidden="until-found"])) {
display: none !important;
}
}
@layer utilities {
.pointer-events-none {
pointer-events: none;
}
.absolute {
position: absolute;
}
.relative {
position: relative;
}
.inset-0 {
inset: 0px;
}
.inset-x-0 {
inset-inline: 0px;
}
.bottom-0 {
bottom: 0px;
}
.flex {
display: flex;
}
.hidden {
display: none;
}
.inline-flex {
display: inline-flex;
}
.h-9 {
height: calc(var(--spacing) * 9);
}
.items-center {
align-items: center;
}
.justify-center {
justify-content: center;
}
.gap-2 {
gap: calc(var(--spacing) * 2);
}
.rounded-\[9px\] {
border-radius: 9px;
}
.border {
border-style: var(--tw-border-style);
border-width: 1px;
}
.border-stone-200 {
border-color: var(--color-stone-200);
}
.bg-stone-300 {
background-color: var(--color-stone-300);
}
.bg-white {
background-color: var(--color-white);
}
.px-3\.5 {
padding-inline: calc(var(--spacing) * 3.5);
}
.align-middle {
vertical-align: middle;
}
.text-\[13px\] {
font-size: 13px;
}
.font-medium {
--tw-font-weight: var(--font-weight-medium);
font-weight: var(--font-weight-medium);
}
.text-stone-700 {
color: var(--color-stone-700);
}
.shadow-\[inset_0_1\.5px_0_rgba\(255\,255\,255\,0\.95\)\,inset_0_-1px_0_rgba\(28\,25\,23\,0\.06\)\] {
--tw-shadow: inset 0 1.5px 0 var(--tw-shadow-color, rgba(255,255,255,0.95)), inset 0 -1px 0 var(--tw-shadow-color, rgba(28,25,23,0.06));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.blur {
--tw-blur: blur(8px);
filter: var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,);
}
.transition {
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter, display, content-visibility, overlay, pointer-events;
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function));
transition-duration: var(--tw-duration, var(--default-transition-duration));
}
.outline-none {
--tw-outline-style: none;
outline-style: none;
}
.select-none {
-webkit-user-select: none;
user-select: none;
}
.group-focus-visible\:ring-2:is(:where(.group):focus-visible *) {
--tw-ring-shadow: var(--tw-ring-inset,) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentcolor);
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.group-focus-visible\:ring-stone-400:is(:where(.group):focus-visible *) {
--tw-ring-color: var(--color-stone-400);
}
.disabled\:opacity-50:disabled {
opacity: 50%;
}
.dark\:border-white\/\[0\.16\]:where(.dark, .dark *) {
border-color: color-mix(in srgb, #fff 16%, transparent);
@supports (color: color-mix(in lab, red, red)) {
border-color: color-mix(in oklab, var(--color-white) 16%, transparent);
}
}
.dark\:bg-\[\#1D1D1A\]:where(.dark, .dark *) {
background-color: #1D1D1A;
}
.dark\:bg-white\/25:where(.dark, .dark *) {
background-color: color-mix(in srgb, #fff 25%, transparent);
@supports (color: color-mix(in lab, red, red)) {
background-color: color-mix(in oklab, var(--color-white) 25%, transparent);
}
}
.dark\:text-stone-200:where(.dark, .dark *) {
color: var(--color-stone-200);
}
.dark\:shadow-\[inset_0_1\.5px_0_rgba\(255\,255\,255\,0\.09\)\]:where(.dark, .dark *) {
--tw-shadow: inset 0 1.5px 0 var(--tw-shadow-color, rgba(255,255,255,0.09));
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
}
.dark\:group-focus-visible\:ring-stone-500:where(.dark, .dark *):is(:where(.group):focus-visible *) {
--tw-ring-color: var(--color-stone-500);
}
}
:root {
--background: #fff;
--foreground: #111;
--primary: #171717;
--primary-foreground: #fff;
--secondary: #eee;
--secondary-foreground: #171717;
--muted: #eee;
--muted-foreground: #666;
--accent: #eee;
--accent-foreground: #171717;
--border: #ddd;
--input: #ddd;
--ring: #777;
--destructive: #dc2626;
--destructive-foreground: #fff;
--popover: #fff;
--popover-foreground: #111;
--success: #15803d;
--success-foreground: #fff;
--radius: .625rem;
--hairline: 1px;
}
.dark {
--background: #090909;
--foreground: #fafafa;
--primary: #fafafa;
--primary-foreground: #171717;
--secondary: #262626;
--secondary-foreground: #fafafa;
--muted: #262626;
--muted-foreground: #a3a3a3;
--accent: #262626;
--accent-foreground: #fafafa;
--border: #333;
--input: #333;
--popover: #171717;
--popover-foreground: #fafafa;
}
:root {
--bezel: #ffffff;
--panel: #ffffff;
--well: #ffffff;
--sub: #fafafa;
--hairline: rgba(0, 0, 0, 0.14);
--hairline-strong: rgba(0, 0, 0, 0.26);
--ink: #131313;
--ink-2: #454545;
--ink-3: #6e6e6e;
--accent: #4568ff;
--accent-soft: #edf1ff;
--moss: #3d7a4e;
--flag: #c0442f;
--shadow-float: 0 28px 56px -24px rgba(0, 0, 0, 0.22),
0 0 0 1px rgba(0, 0, 0, 0.06);
--shadow-lift: 0 1px 2px rgba(0, 0, 0, 0.04),
0 0 0 1px rgba(0, 0, 0, 0.11);
--shadow-well: inset 0 1px 2px rgba(0, 0, 0, 0.05),
inset 0 0 0 1px rgba(0, 0, 0, 0.1);
--shadow-cap: 0 1.5px 0 rgba(0, 0, 0, 0.09),
inset 0 0 0 1px rgba(0, 0, 0, 0.11);
--shadow-row: inset 0 0 0 1px rgba(0, 0, 0, 0.07),
0 1px 2px rgba(0, 0, 0, 0.05);
}
.dark {
--bezel: #141312;
--panel: #1d1d1a;
--well: #252522;
--sub: #1a1a17;
--hairline: rgba(255, 255, 255, 0.16);
--hairline-strong: rgba(255, 255, 255, 0.28);
--ink: #f8f8f4;
--ink-2: #c2c2ba;
--ink-3: #93938b;
--accent: #93b0ff;
--accent-soft: rgba(147, 176, 255, 0.14);
--moss: #5bd79c;
--flag: #f5897f;
--shadow-float: 0 28px 56px -22px rgba(0, 0, 0, 0.7),
0 0 0 1px rgba(255, 255, 255, 0.05);
--shadow-lift: 0 1px 2px rgba(0, 0, 0, 0.4),
inset 0 1px 0 rgba(255, 255, 255, 0.05),
0 0 0 1px rgba(255, 255, 255, 0.05);
--shadow-well: inset 0 1px 3px rgba(0, 0, 0, 0.4),
inset 0 0 0 1px rgba(255, 255, 255, 0.03);
--shadow-cap: 0 1.5px 0 rgba(0, 0, 0, 0.5),
inset 0 0 0 1px rgba(255, 255, 255, 0.1);
--shadow-row: inset 0 0 0 1px rgba(255, 255, 255, 0.06),
0 1px 2px rgba(0, 0, 0, 0.25);
}
@keyframes reaction-drift {
to {
transform: translateX(-50%);
}
}
@property --tw-border-style {
syntax: "*";
inherits: false;
initial-value: solid;
}
@property --tw-font-weight {
syntax: "*";
inherits: false;
}
@property --tw-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-shadow-color {
syntax: "*";
inherits: false;
}
@property --tw-shadow-alpha {
syntax: "<percentage>";
inherits: false;
initial-value: 100%;
}
@property --tw-inset-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-inset-shadow-color {
syntax: "*";
inherits: false;
}
@property --tw-inset-shadow-alpha {
syntax: "<percentage>";
inherits: false;
initial-value: 100%;
}
@property --tw-ring-color {
syntax: "*";
inherits: false;
}
@property --tw-ring-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-inset-ring-color {
syntax: "*";
inherits: false;
}
@property --tw-inset-ring-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-ring-inset {
syntax: "*";
inherits: false;
}
@property --tw-ring-offset-width {
syntax: "<length>";
inherits: false;
initial-value: 0px;
}
@property --tw-ring-offset-color {
syntax: "*";
inherits: false;
initial-value: #fff;
}
@property --tw-ring-offset-shadow {
syntax: "*";
inherits: false;
initial-value: 0 0 #0000;
}
@property --tw-blur {
syntax: "*";
inherits: false;
}
@property --tw-brightness {
syntax: "*";
inherits: false;
}
@property --tw-contrast {
syntax: "*";
inherits: false;
}
@property --tw-grayscale {
syntax: "*";
inherits: false;
}
@property --tw-hue-rotate {
syntax: "*";
inherits: false;
}
@property --tw-invert {
syntax: "*";
inherits: false;
}
@property --tw-opacity {
syntax: "*";
inherits: false;
}
@property --tw-saturate {
syntax: "*";
inherits: false;
}
@property --tw-sepia {
syntax: "*";
inherits: false;
}
@property --tw-drop-shadow {
syntax: "*";
inherits: false;
}
@property --tw-drop-shadow-color {
syntax: "*";
inherits: false;
}
@property --tw-drop-shadow-alpha {
syntax: "<percentage>";
inherits: false;
initial-value: 100%;
}
@property --tw-drop-shadow-size {
syntax: "*";
inherits: false;
}
@layer properties {
@supports ((-webkit-hyphens: none) and (not (margin-trim: inline))) or ((-moz-orient: inline) and (not (color:rgb(from red r g b)))) {
*, ::before, ::after, ::backdrop {
--tw-border-style: solid;
--tw-font-weight: initial;
--tw-shadow: 0 0 #0000;
--tw-shadow-color: initial;
--tw-shadow-alpha: 100%;
--tw-inset-shadow: 0 0 #0000;
--tw-inset-shadow-color: initial;
--tw-inset-shadow-alpha: 100%;
--tw-ring-color: initial;
--tw-ring-shadow: 0 0 #0000;
--tw-inset-ring-color: initial;
--tw-inset-ring-shadow: 0 0 #0000;
--tw-ring-inset: initial;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-offset-shadow: 0 0 #0000;
--tw-blur: initial;
--tw-brightness: initial;
--tw-contrast: initial;
--tw-grayscale: initial;
--tw-hue-rotate: initial;
--tw-invert: initial;
--tw-opacity: initial;
--tw-saturate: initial;
--tw-sepia: initial;
--tw-drop-shadow: initial;
--tw-drop-shadow-color: initial;
--tw-drop-shadow-alpha: 100%;
--tw-drop-shadow-size: initial;
}
}
}