EmpireUI
Get Pro
← Blog8 min read#claymorphism#button#3d

Claymorphism Button: 3D Clay Press Animation in CSS

Build a claymorphism button with a satisfying 3D press animation using pure CSS — layered box-shadows, transform, and transition tricks that actually feel tactile.

colorful 3D clay-style button UI component on pastel background

What Makes a Button Feel Like Clay

Claymorphism is the design trend that took off around 2022 and hasn't really slowed down. It's the one where UI elements look like they were physically pressed out of soft, colorful clay — rounded to the point of almost being circles, lit from above, with chunky shadows that make you want to reach through the screen and push them. Buttons are where this style absolutely shines.

The secret sauce isn't a single CSS property. It's a combination of three things working together: a multi-layered box-shadow that simulates a thick clay extrusion, a border-radius pushed well past what you'd normally use (we're talking 16px at minimum, often 24px or more), and a transform: translateY() on :active that physically sinks the button down when you press it — collapsing the shadow as it goes. Get all three right and you've got something that feels genuinely tactile.

Honestly, the press animation is the part most tutorials skip. They show you the static look and call it done. But without that active-state sinking motion, you're just building a fat rounded button. The animation is the whole personality.

Worth noting: claymorphism works with or without JavaScript. Everything we're building here runs on pure CSS — no React state, no JS event listeners, no animation libraries. Just :hover, :active, and transition. That matters for performance and for keeping your component portable.

The CSS Box-Shadow Stack That Creates 3D Depth

A flat button has one box-shadow, maybe two. A clay button has four. Each layer in the stack does a different job, and once you understand the layering, you can tune the look to exactly the feel you want.

Here's the full shadow recipe — the numbers aren't arbitrary. The 0px 8px 0px offset is what creates the hard bottom edge that reads as thickness. The 0px 10px 20px blur adds the soft ambient shadow underneath. The inset shadows handle the top highlight and bottom interior shading that sell the three-dimensional surface: ``css .clay-button { /* Base geometry */ padding: 14px 28px; border-radius: 16px; border: none; cursor: pointer; /* Clay fill */ background: #6c63ff; color: white; font-size: 1rem; font-weight: 700; letter-spacing: 0.02em; /* The 4-layer shadow stack */ box-shadow: /* 1. Hard offset — this IS the clay thickness */ 0px 8px 0px #4b44cc, /* 2. Soft ambient beneath the button */ 0px 12px 20px rgba(108, 99, 255, 0.35), /* 3. Top surface highlight (inset) */ inset 0px 2px 4px rgba(255, 255, 255, 0.4), /* 4. Bottom interior shadow (inset) */ inset 0px -2px 4px rgba(0, 0, 0, 0.15); /* Smooth the press transition */ transition: transform 80ms ease-out, box-shadow 80ms ease-out; /* Stop text selection on rapid clicks */ user-select: none; } ``

That 80ms transition duration is intentional. Go much slower and the button feels laggy. Go faster and you lose the satisfying squish. 80ms sits right on the edge of perceptible — it's fast enough to feel responsive, slow enough to read as physical.

The darker color for the hard offset shadow (#4b44cc in the example) should be around 20-30% darker than your main button color. Generate the right tone with the gradient generator — it also lets you preview hue-shifted versions of your palette which is great for this kind of shadow matching.

The Active State: Making It Actually Press Down

This is the part that transforms a styled button into a claymorphism button. On :active, you do three things simultaneously: translate the button down by the same amount as the hard shadow offset, reduce that offset to zero, and pull back the ambient shadow. The button appears to physically sink into the page.

.clay-button:hover {
  /* Slight lift on hover — optional but nice */
  transform: translateY(-2px);
  box-shadow:
    0px 10px 0px #4b44cc,
    0px 16px 24px rgba(108, 99, 255, 0.40),
    inset 0px 2px 4px rgba(255, 255, 255, 0.4),
    inset 0px -2px 4px rgba(0, 0, 0, 0.15);
}

.clay-button:active {
  /* Sink DOWN by the full shadow offset */
  transform: translateY(8px);

  /* Collapse the hard offset to zero */
  box-shadow:
    0px 0px 0px #4b44cc,
    /* Soften the ambient — button is closer to surface */
    0px 4px 8px rgba(108, 99, 255, 0.25),
    inset 0px 2px 4px rgba(255, 255, 255, 0.3),
    /* Deepen interior shadow when pressed */
    inset 0px -4px 8px rgba(0, 0, 0, 0.25);
}

The translateY(8px) matches the 8px hard offset exactly. That's the key relationship — if your hard shadow is 0px 12px 0px, your active transform should be translateY(12px). They move together: as the button sinks, the shadow collapses beneath it.

Look, a lot of designers add a scale(0.97) on active too, compressing the button slightly like real clay being squished. It works. It's a personal preference call. I'd say skip it for small buttons (under 40px height) because the scale change barely reads — but for big hero CTAs, that subtle squish adds a lot.

One more thing — keyboard users need love too. Your :focus-visible state should match the :hover visual so keyboard navigation feels deliberate: ``css .clay-button:focus-visible { outline: 3px solid rgba(108, 99, 255, 0.6); outline-offset: 4px; transform: translateY(-2px); } ``

Building the React Component

Pure CSS is great for quick prototypes. Production React apps want a typed, composable component with variant support. Here's a version that handles four color variants plus a disabled state — all without touching a single JS animation library: ``tsx // ClayButton.tsx import { ButtonHTMLAttributes } from 'react'; type Variant = 'purple' | 'coral' | 'mint' | 'amber'; const variants: Record<Variant, { bg: string; shadow: string; active: string }> = { purple: { bg: '#6c63ff', shadow: '#4b44cc', active: 'rgba(108,99,255,0.35)', }, coral: { bg: '#ff6b6b', shadow: '#cc4444', active: 'rgba(255,107,107,0.35)', }, mint: { bg: '#51cf66', shadow: '#2f9e44', active: 'rgba(81,207,102,0.35)', }, amber: { bg: '#ffd43b', shadow: '#e67700', active: 'rgba(255,212,59,0.35)', }, }; interface ClayButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> { variant?: Variant; } export function ClayButton({ variant = 'purple', children, disabled, className = '', ...rest }: ClayButtonProps) { const v = variants[variant]; return ( <button {...rest} disabled={disabled} className={clay-btn clay-btn--${variant} ${disabled ? 'clay-btn--disabled' : ''} ${className}} style={{ '--clay-bg': v.bg, '--clay-shadow': v.shadow, '--clay-glow': v.active, } as React.CSSProperties} > {children} </button> ); } ``

Then in your global CSS (or a .module.css file), drive everything from those custom properties: ``css .clay-btn { padding: 14px 32px; border-radius: 16px; border: none; cursor: pointer; background: var(--clay-bg); color: white; font-size: 1rem; font-weight: 700; box-shadow: 0px 8px 0px var(--clay-shadow), 0px 12px 20px var(--clay-glow), inset 0px 2px 4px rgba(255,255,255,0.4), inset 0px -2px 4px rgba(0,0,0,0.15); transition: transform 80ms ease-out, box-shadow 80ms ease-out; user-select: none; } .clay-btn:hover { transform: translateY(-2px); } .clay-btn:active { transform: translateY(8px); box-shadow: 0px 0px 0px var(--clay-shadow), 0px 4px 8px var(--clay-glow), inset 0px 2px 4px rgba(255,255,255,0.3), inset 0px -4px 8px rgba(0,0,0,0.25); } .clay-btn--disabled { opacity: 0.5; cursor: not-allowed; pointer-events: none; } ``

The CSS custom property trick is clean. You get full theme-ability without a single styled-components dependency or Tailwind arbitrary value. And because the properties live on the element itself, you can override them inline for one-off custom colors.

Quick aside: if you're using Tailwind and want to avoid the separate CSS file, the arbitrary value syntax handles this fine — shadow-[0px_8px_0px_#4b44cc,0px_12px_20px_rgba(108,99,255,0.35)] — but it gets unwieldy fast. For anything beyond a single button, the custom property approach above is cleaner. You can also browse claymorphism components on Empire UI and grab pre-built variants without writing any of this by hand.

Tailwind Variant: Quick Implementation

If you're working in a Tailwind-heavy codebase and want to stay in utility-class land, you can pull this off with a small Tailwind plugin in tailwind.config.js. The plugin adds clay-{color} variants that you can apply to any button: ``js // tailwind.config.js const plugin = require('tailwindcss/plugin'); module.exports = { plugins: [ plugin(({ addComponents }) => { addComponents({ '.clay-btn': { padding: '14px 32px', borderRadius: '16px', border: 'none', cursor: 'pointer', color: 'white', fontWeight: '700', userSelect: 'none', transition: 'transform 80ms ease-out, box-shadow 80ms ease-out', '&:hover': { transform: 'translateY(-2px)' }, '&:active': { transform: 'translateY(8px)', }, }, '.clay-purple': { background: '#6c63ff', boxShadow: '0px 8px 0px #4b44cc, 0px 12px 20px rgba(108,99,255,0.35), inset 0 2px 4px rgba(255,255,255,0.4), inset 0 -2px 4px rgba(0,0,0,0.15)', '&:active': { boxShadow: '0px 0px 0px #4b44cc, 0px 4px 8px rgba(108,99,255,0.25), inset 0 2px 4px rgba(255,255,255,0.3), inset 0 -4px 8px rgba(0,0,0,0.25)', }, }, }); }), ], }; ``

Then usage is just <button class="clay-btn clay-purple">Click me</button>. That's it. The :active shadow collapse is handled inside the plugin, so you don't have to think about it at the usage site.

In practice, the plugin approach scales better than arbitrary values for this particular effect because the shadow stack is too complex to be readable inline. You'd spend more time debugging a misplaced underscore in a Tailwind arbitrary value than it'd take to just write the CSS properly.

That said, if you just need one button and want zero build configuration, the hand-written CSS from the earlier sections is perfectly fine. Don't over-engineer it.

Accessibility, Reduced Motion, and Edge Cases

The press animation is driven by transform: translateY() — that's perfectly safe from an accessibility standpoint because it's not causing content to reflow or obscure other elements. But you still need to handle prefers-reduced-motion. Users who have requested reduced motion shouldn't see the button physically bouncing around: ``css @media (prefers-reduced-motion: reduce) { .clay-btn, .clay-btn:hover, .clay-btn:active { transform: none; transition: none; } } ``

Color contrast is the bigger concern with claymorphism's vibrant palette. That amber variant from earlier (#ffd43b background, white text) fails WCAG AA — yellow and white is a notorious pairing. Switch amber buttons to dark text (color: #1a1a1a) and check contrast with your browser's DevTools accessibility panel. Chrome DevTools added inline contrast ratio display in 2023 and it's genuinely useful here.

There's also the question of what happens when the button is inside a container with overflow: hidden. The translateY(8px) on press will get clipped. Either remove overflow: hidden from the parent or add 8px of bottom padding to create room for the press travel. This trips up a lot of people who build the button in isolation and then drop it into a card component.

Disabled states deserve a mention too. The pointer-events: none on .clay-btn--disabled means keyboard users also can't focus the button, which can be confusing. A better pattern is to use the native disabled attribute and let the browser manage focus — just add &:disabled { opacity: 0.5; cursor: not-allowed; transform: none; } and don't set pointer-events: none.

You can pair these buttons with Empire UI's full claymorphism component set — they ship with accessible variants already configured, so you're not starting from scratch on any of this. And the box shadow generator is great for experimenting with different shadow offsets and glow amounts before committing to a specific look.

Taking Clay Buttons Further

Once you've got the base press animation working, there are a few extensions worth knowing about. Icon buttons at 48px diameter or smaller need border-radius: 50% instead of 16px to go fully circular — but the shadow stack and active-state math stays identical. Same translateY(8px), same 0px 8px 0px hard offset. Just a border-radius change.

Loading states are fun to animate on clay buttons. A pulsing scale (@keyframes clay-pulse { 0%, 100% { transform: scale(1); } 50% { transform: scale(0.97); } }) while the button is in a loading state gives you that gentle breathing feel without breaking the clay metaphor. Disable the :active press while loading so users can't interact with a button that's already processing.

For group layouts — a row of clay buttons like a tab bar or toggle group — keep 8px of gap between buttons so the drop shadows don't visually merge. When shadows overlap the depth illusion breaks and everything flattens out. 8px is the minimum; 12px gives you room to breathe.

If you want to go deeper into the claymorphism visual language beyond buttons — cards, modals, navigation — the claymorphism hub has the full component library. And if you're building a complete project, the templates section includes landing pages with claymorphism theming already applied so you can see how these buttons fit into a full layout. The style works especially well alongside the gradient generator for picking background colors that make the clay surfaces pop.

FAQ

Why does my clay button shadow get clipped when I press it?

The translateY(8px) on press is almost certainly being cut by a parent container with overflow: hidden. Add padding-bottom: 8px to the parent or remove overflow: hidden from whatever's wrapping the button.

What's the right border-radius for claymorphism buttons?

Between 12px and 24px for rectangular buttons — 16px is a solid default. For square icon buttons, use border-radius: 50% to go fully circular. Anything below 12px and you lose the clay softness.

Can I do the clay press effect with Tailwind only, no custom CSS?

Technically yes, using arbitrary values, but the shadow stack is complex enough that it becomes unreadable. A small Tailwind plugin or a single CSS class file is a much better call for maintainability.

Does the active-state translateY value always have to match the hard shadow offset?

Yes — that relationship is what makes the button look like it's physically sinking. If your hard shadow is 0px 12px 0px, your active transform must be translateY(12px) or the motion will look disconnected.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Claymorphism Navbar: Soft 3D Navigation With Clay ButtonsClaymorphism Button in React: Puffy 3D Buttons with CSS box-shadowCSS Flip Card: 3D Rotate Animation With and Without JavaScriptLiquid Fill Button Animation in CSS: SVG and clip-path Morph