Pure TailwindMIT licensed

Tailwind 3D Press Effect Button with Keycap Depth

A keycap that physically sinks into its own platform. Two properties do all the work, and as utilities the resting and pressed states sit side by side: shadow-[0_6px_0_…] becomes active:shadow-[0_2px_0_…] while the face drops active:translate-y-1.

Live Demo
Plain CSSTailwind v4
Try it

Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time

Untitled UI React · Affiliate link

Get Untitled UI Pro

The code

<section class="tab-05 w-full min-h-svh grid place-items-center bg-tab05-bg font-tab05-sans px-6 py-10">
  <div class="flex flex-col items-center gap-10">
    <p class="text-[0.78rem] tracking-[0.12em] uppercase text-tab05-mut m-0">Click or tap the buttons</p>
    <div class="flex flex-wrap justify-center gap-5">
      <button type="button" class="min-h-11 px-9 text-base font-bold tracking-[0.03em] text-white bg-tab05-indigo rounded-[10px] cursor-pointer select-none transition-[translate,box-shadow] duration-[80ms] shadow-[0_6px_0_var(--color-tab05-indigo-deep),0_8px_12px_oklch(0_0_0/0.18)] hover:brightness-105 active:translate-y-1 active:shadow-[0_2px_0_var(--color-tab05-indigo-deep),0_4px_6px_oklch(0_0_0/0.12)] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab05-indigo motion-reduce:transition-none">Purchase Now</button>
      <button type="button" class="min-h-11 px-9 text-base font-bold tracking-[0.03em] text-white bg-tab05-emerald rounded-[10px] cursor-pointer select-none transition-[translate,box-shadow] duration-[80ms] shadow-[0_6px_0_var(--color-tab05-emerald-deep),0_8px_12px_oklch(0_0_0/0.18)] hover:brightness-105 active:translate-y-1 active:shadow-[0_2px_0_var(--color-tab05-emerald-deep),0_4px_6px_oklch(0_0_0/0.12)] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab05-emerald motion-reduce:transition-none">Download App</button>
      <button type="button" class="min-h-11 px-9 text-base font-bold tracking-[0.03em] text-white bg-tab05-rose rounded-[10px] cursor-pointer select-none transition-[translate,box-shadow] duration-[80ms] shadow-[0_6px_0_var(--color-tab05-rose-deep),0_8px_12px_oklch(0_0_0/0.18)] hover:brightness-105 active:translate-y-1 active:shadow-[0_2px_0_var(--color-tab05-rose-deep),0_4px_6px_oklch(0_0_0/0.12)] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab05-rose motion-reduce:transition-none">Delete File</button>
    </div>
  </div>
</section>
@import "tailwindcss";

@theme {
  --color-tab05-bg: #f8fafc;
  --color-tab05-bg: oklch(0.98 0.004 250);
  --color-tab05-mut: #64748b;
  --color-tab05-mut: oklch(0.55 0.03 255);
  --color-tab05-indigo: #6366f1;
  --color-tab05-indigo: oklch(0.58 0.21 277);
  --color-tab05-indigo-deep: #3730a3;
  --color-tab05-indigo-deep: oklch(0.38 0.17 277);
  --color-tab05-emerald: #10b981;
  --color-tab05-emerald: oklch(0.7 0.15 163);
  --color-tab05-emerald-deep: #065f46;
  --color-tab05-emerald-deep: oklch(0.42 0.09 163);
  --color-tab05-rose: #f43f5e;
  --color-tab05-rose: oklch(0.63 0.22 15);
  --color-tab05-rose-deep: #9f1239;
  --color-tab05-rose-deep: oklch(0.42 0.16 15);
  --font-tab05-sans: 'Space Grotesk', system-ui, sans-serif;
}

.tab-05 {
  width: 100%;
  min-height: 100svh;
}

.tab-05 ::selection {
  background: var(--color-tab05-indigo);
  color: #fff;
}

Setup — do this once per project

Skip this if your project already runs Tailwind v4.

1Load Tailwind
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>

Compiles in the browser — perfect for a CodePen or a quick test, not for production.

2Add the stylesheetRequiredthe @theme block is what makes the custom utilities exist
@import "tailwindcss";

@theme {
  --color-tab05-bg: #f8fafc;
  --color-tab05-bg: oklch(0.98 0.004 250);
  --color-tab05-mut: #64748b;
  --color-tab05-mut: oklch(0.55 0.03 255);
  --color-tab05-indigo: #6366f1;
  --color-tab05-indigo: oklch(0.58 0.21 277);
  --color-tab05-indigo-deep: #3730a3;
  --color-tab05-indigo-deep: oklch(0.38 0.17 277);
  --color-tab05-emerald: #10b981;
  --color-tab05-emerald: oklch(0.7 0.15 163);
  --color-tab05-emerald-deep: #065f46;
  --color-tab05-emerald-deep: oklch(0.42 0.09 163);
  --color-tab05-rose: #f43f5e;
  --color-tab05-rose: oklch(0.63 0.22 15);
  --color-tab05-rose-deep: #9f1239;
  --color-tab05-rose-deep: oklch(0.42 0.16 15);
  --font-tab05-sans: 'Space Grotesk', system-ui, sans-serif;
}

.tab-05 {
  width: 100%;
  min-height: 100svh;
}

.tab-05 ::selection {
  background: var(--color-tab05-indigo);
  color: #fff;
}
3Fonts used by this demooptional — skip if you have your own type stack
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;600;700&display=swap">

What changed in the port

Which parts became utilities, which stayed CSS, and why.

  • The face/depth colour pair becomes two @theme entries per variant, and the shadow utility reads both by name. The CSS version used two local custom properties per button; tokens make the pairing reusable across a real design system.
  • transition-[translate,box-shadow] rather than transition-all: the press must not accidentally tween filter, which the hover state owns.
  • UI change from the CSS original: a focus-visible ring with 4px offset, clear of the depth shadow so it is not lost against the underhang.
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind 3D Press Effect Button with Keycap Depth
Source: https://codefronts.com/motion/tailwind-css-animated-buttons/tailwind-3d-press-effect-button/

A keycap that physically sinks into its own platform. Two properties do all the work, and as utilities the resting and pressed states sit side by side: shadow-[0_6px_0_…] becomes active:shadow-[0_2px_0_…] while the face drops active:translate-y-1.
## HTML
```html
<section class="tab-05 w-full min-h-svh grid place-items-center bg-tab05-bg font-tab05-sans px-6 py-10">
  <div class="flex flex-col items-center gap-10">
    <p class="text-[0.78rem] tracking-[0.12em] uppercase text-tab05-mut m-0">Click or tap the buttons</p>
    <div class="flex flex-wrap justify-center gap-5">
      <button type="button" class="min-h-11 px-9 text-base font-bold tracking-[0.03em] text-white bg-tab05-indigo rounded-[10px] cursor-pointer select-none transition-[translate,box-shadow] duration-[80ms] shadow-[0_6px_0_var(--color-tab05-indigo-deep),0_8px_12px_oklch(0_0_0/0.18)] hover:brightness-105 active:translate-y-1 active:shadow-[0_2px_0_var(--color-tab05-indigo-deep),0_4px_6px_oklch(0_0_0/0.12)] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab05-indigo motion-reduce:transition-none">Purchase Now</button>
      <button type="button" class="min-h-11 px-9 text-base font-bold tracking-[0.03em] text-white bg-tab05-emerald rounded-[10px] cursor-pointer select-none transition-[translate,box-shadow] duration-[80ms] shadow-[0_6px_0_var(--color-tab05-emerald-deep),0_8px_12px_oklch(0_0_0/0.18)] hover:brightness-105 active:translate-y-1 active:shadow-[0_2px_0_var(--color-tab05-emerald-deep),0_4px_6px_oklch(0_0_0/0.12)] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab05-emerald motion-reduce:transition-none">Download App</button>
      <button type="button" class="min-h-11 px-9 text-base font-bold tracking-[0.03em] text-white bg-tab05-rose rounded-[10px] cursor-pointer select-none transition-[translate,box-shadow] duration-[80ms] shadow-[0_6px_0_var(--color-tab05-rose-deep),0_8px_12px_oklch(0_0_0/0.18)] hover:brightness-105 active:translate-y-1 active:shadow-[0_2px_0_var(--color-tab05-rose-deep),0_4px_6px_oklch(0_0_0/0.12)] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-tab05-rose motion-reduce:transition-none">Delete File</button>
    </div>
  </div>
</section>
```
## CSS
```css
@import "tailwindcss";

@theme {
  --color-tab05-bg: #f8fafc;
  --color-tab05-bg: oklch(0.98 0.004 250);
  --color-tab05-mut: #64748b;
  --color-tab05-mut: oklch(0.55 0.03 255);
  --color-tab05-indigo: #6366f1;
  --color-tab05-indigo: oklch(0.58 0.21 277);
  --color-tab05-indigo-deep: #3730a3;
  --color-tab05-indigo-deep: oklch(0.38 0.17 277);
  --color-tab05-emerald: #10b981;
  --color-tab05-emerald: oklch(0.7 0.15 163);
  --color-tab05-emerald-deep: #065f46;
  --color-tab05-emerald-deep: oklch(0.42 0.09 163);
  --color-tab05-rose: #f43f5e;
  --color-tab05-rose: oklch(0.63 0.22 15);
  --color-tab05-rose-deep: #9f1239;
  --color-tab05-rose-deep: oklch(0.42 0.16 15);
  --font-tab05-sans: 'Space Grotesk', system-ui, sans-serif;
}

.tab-05 {
  width: 100%;
  min-height: 100svh;
}

.tab-05 ::selection {
  background: var(--color-tab05-indigo);
  color: #fff;
}
```

How this works

The depth is a shadow with zero blur. 0 6px 0 in a darker shade of the face reads as solid material under the button. Pressing moves the face down 4px and shrinks the underhang to 2px, so the total height stays honest and the gap appears compressed.

80ms is the technique. Real keys have no easing you can perceive. The utility version states the duration where the press states are, which makes it obvious that this is deliberately far shorter than the 200-300ms used elsewhere in the collection.

Keyboard users get the press too. focus-visible: supplies a ring, and active: fires on Space as well as pointer press — so the tactile payload is not mouse-only.

Make it yours

  • Deeper travel: shadow-[0_10px_0_…] with active:translate-y-2 — keep the two numbers in step.
  • New colour: set a face token and a depth token roughly 20% darker in the same hue.
  • Add a top bevel with an extra inset_0_1px_0_oklch(1_0_0/0.25) layer in the resting shadow.

Gotchas — read before shipping

  • Inside an already-transformed ancestor the active:translate-y-1 composes with the parent transform — always test in context, not in isolation.
  • If the depth shadow and the press translate disagree by even a pixel the button appears to bounce; change them together.
  • select-none is not cosmetic here. Without it, a fast double-tap selects the label and drops the :active state mid-press.

Browser support

ChromeSafariFirefoxEdge
111+16.4+128+111+

Transforms and box-shadows are universally supported; the floor is Tailwind v4's generated sheet.

Techniques used in this demo

Search CodeFronts

Loading…