Claymorphism Card Components: 3D Puffy Cards With Soft Shadows
Build 3D puffy claymorphism card components in React using layered box shadows and pastel backgrounds — with copy-paste CSS and ready-to-use component patterns.
What Makes a Card Look Like Clay
Cards are everywhere. Almost every UI you'll build has at least one. But most cards feel flat — a white box, maybe a light border, a subtle shadow if the designer was feeling adventurous. Claymorphism cards go the opposite direction. They look like someone pressed a lump of colourful dough into the screen.
The effect comes from a very specific stacking of box-shadow layers. You need at least four. A white inset highlight at the top-left edge simulates light coming from above. A dark inset shadow at the bottom-right anchors the object. An outer coloured drop shadow — matching the card's background colour — gives it that floaty elevation. And a neutral dark shadow underneath for base depth. Get all four right and the inflation illusion just works.
Honestly, the inset highlights are the part most developers skip, and that's why their clay cards look wrong. Without the inset white at the top, you just have a coloured rounded card with a drop shadow — which looks fine but isn't clay. The inset is what sells the rounded, puffy surface catching overhead light. It takes one extra line of CSS.
Worth noting: border radius matters as much as the shadows. You want border-radius somewhere between 1.5rem and 3rem for cards. Below that range it looks like a rounded corner. Above it you're into pill or blob territory. The sweet spot for a standard-size card is around 24px to 28px — which is exactly what the Empire UI claymorphism preset ships with.
The CSS Shadow Stack: Copy This Exactly
Stop guessing at shadow values. Here's the pattern that actually works for a clay card. The colour variable (--clay-color) drives the entire thing — change one hex and every shadow adapts automatically:
``css
.clay-card {
--clay-color: #a78bfa; /* violet — swap freely */
--clay-color-shadow: #7c3aed; /* darker tint for outer shadow */
background: var(--clay-color);
border-radius: 1.75rem; /* 28px */
padding: 2rem;
color: #fff;
box-shadow:
/* 1. top highlight — white inset, sells the rounded surface */
inset 0 6px 12px rgba(255, 255, 255, 0.50),
/* 2. bottom anchor — dark inset, adds base depth */
inset 0 -6px 12px rgba(0, 0, 0, 0.18),
/* 3. coloured lift — floats the card off the page */
0 16px 40px -4px rgba(124, 58, 237, 0.55),
/* 4. neutral base — grounds it */
0 6px 16px rgba(0, 0, 0, 0.10);
transition:
transform 0.18s cubic-bezier(0.34, 1.56, 0.64, 1),
box-shadow 0.18s ease;
}
.clay-card:hover {
transform: translateY(-6px) scale(1.015);
box-shadow:
inset 0 6px 12px rgba(255, 255, 255, 0.50),
inset 0 -6px 12px rgba(0, 0, 0, 0.18),
0 28px 56px -4px rgba(124, 58, 237, 0.60),
0 10px 24px rgba(0, 0, 0, 0.12);
}
``
The cubic-bezier (0.34, 1.56, 0.64, 1) on the hover transform gives you a spring overshoot — the card lifts slightly past its destination then settles back. That 1.56 value is what makes it feel bouncy rather than mechanical. Drop it to ease-out and you lose the clay personality entirely.
One more thing — that -4px spread on the coloured lift shadow. Negative spread makes the shadow tighter and more defined at the source, then it feathers out over the blur distance. Without it the coloured shadow blooms into a big soft glow that looks more like neon than clay. Keep that -4px in there.
Quick aside: if your background isn't white or near-white, the coloured lift shadow might clash with it. In that case, pull the shadow opacity down to 0.35–0.40 and let the neutral base shadow carry more of the elevation weight. Clay cards work best over light backgrounds — cream, light grey, or white.
A Reusable React ClayCard Component
Pure CSS is fine for static prototypes. In a real React codebase you want a component that accepts colour as a prop and derives all the shadow math automatically. Here's one that does that without any shadow value duplication:
``tsx
// ClayCard.tsx
import React, { CSSProperties } from 'react';
interface ClayCardProps {
children: React.ReactNode;
color?: string; // hex, e.g. '#a78bfa'
shadowColor?: string; // optional darker tint for lift shadow
className?: string;
onClick?: () => void;
}
function hexToRgba(hex: string, alpha: number): string {
const r = parseInt(hex.slice(1, 3), 16);
const g = parseInt(hex.slice(3, 5), 16);
const b = parseInt(hex.slice(5, 7), 16);
return rgba(${r},${g},${b},${alpha});
}
export function ClayCard({
children,
color = '#7dd3fc',
shadowColor,
className = '',
onClick,
}: ClayCardProps) {
const lift = shadowColor ?? color;
const style: CSSProperties = {
background: color,
borderRadius: '1.75rem',
boxShadow: [
'inset 0 6px 12px rgba(255,255,255,0.50)',
'inset 0 -6px 12px rgba(0,0,0,0.18)',
0 16px 40px -4px ${hexToRgba(lift, 0.55)},
'0 6px 16px rgba(0,0,0,0.10)',
].join(', '),
};
return (
<div
className={p-6 cursor-default select-none ${className}}
style={style}
onClick={onClick}
>
{children}
</div>
);
}
``
That hexToRgba helper is the key move. You pass a single color prop and every shadow layer gets derived from it — you never write a raw rgba() value by hand. If your palette has a distinct darker tint (say, #6d28d9 for violet or #0369a1 for sky blue), pass it as shadowColor for a richer lift effect. Otherwise the component self-derives from the base colour.
In practice, I'd add whileHover from Framer Motion instead of CSS transitions on the container — it gives you that spring bounce without juggling cubic-bezier values in a stylesheet. But the CSS approach above works perfectly well for most projects and adds zero bundle weight. Pick whichever fits your stack.
You can browse more component patterns and drop them directly into your project via Empire UI — the library ships 40 visual styles and you can preview everything in the claymorphism preset before copying a single line.
Clay Card Variants: Pastel Palette That Actually Works
Clay cards live or die by their colour palette. You can't just throw any hex at the pattern and expect it to read as clay. The colours need to be saturated but not neon, light enough that white text passes WCAG AA at 4.5:1, and warm enough that the inset highlights don't look washed out. Here's the palette that hits all three:
``tsx
// clay-palette.ts
export const CLAY_PALETTE = [
{ name: 'violet', color: '#a78bfa', shadow: '#7c3aed' },
{ name: 'sky', color: '#7dd3fc', shadow: '#0369a1' },
{ name: 'rose', color: '#fda4af', shadow: '#be123c' },
{ name: 'mint', color: '#6ee7b7', shadow: '#047857' },
{ name: 'amber', color: '#fcd34d', shadow: '#b45309' },
{ name: 'coral', color: '#fb923c', shadow: '#9a3412' },
{ name: 'lilac', color: '#c4b5fd', shadow: '#6d28d9' },
{ name: 'peach', color: '#fdba74', shadow: '#c2410c' },
] as const;
``
All of those are from the Tailwind 300–400 range, which is designed to sit in exactly the right brightness zone. Going to 200 makes the colour too pale — the shadow contrast drops and the clay puffiness starts to disappear. Going to 500 and above means your white text might fail contrast checks on the lighter hues like amber and mint. Check every colour at your target size with a contrast checker — don't assume the palette.
Look, the amber card is tricky. At #fcd34d with white text you're at about 1.9:1 ratio — way below WCAG AA. Use dark text (#78350f) on that one specifically. Alternatively, reserve amber for decorative cards with no critical text and put the important content on a violet or sky card instead. This is one of those things that bites you in a design review if you haven't tested it.
You can experiment with these palettes interactively using the gradient generator — it won't give you clay shadows directly, but it's an excellent way to audition background colours against your page's overall colour scheme before you commit.
Adding Press Feedback and Click Animation
A clay card that doesn't react when you click it feels wrong. The whole point of the aesthetic is that it looks like physical material — and physical material compresses when you press it. Adding a press-down animation is six lines:
``css
.clay-card:active {
transform: translateY(2px) scale(0.98);
box-shadow:
inset 0 6px 12px rgba(255, 255, 255, 0.40),
inset 0 -3px 8px rgba(0, 0, 0, 0.20),
0 6px 16px -4px rgba(124, 58, 237, 0.40),
0 2px 6px rgba(0, 0, 0, 0.12);
transition-duration: 0.08s;
}
``
Notice the :active state reduces the outer lift shadow significantly — from 0 16px 40px down to 0 6px 16px. That's the visual equivalent of the card being pushed into the surface. The scale to 0.98 is subtle but important — you feel it more than you see it, and it's what separates a polished interaction from a basic one. The transition-duration: 0.08s on active makes the press snap in instantly while the release (handled by the base transition at 0.18s) springs back more slowly.
If you're using Framer Motion in React 19, whileTap makes this even cleaner:
``tsx
import { motion } from 'framer-motion';
<motion.div
className="clay-card"
whileHover={{ y: -6, scale: 1.015 }}
whileTap={{ y: 2, scale: 0.98 }}
transition={{ type: 'spring', stiffness: 400, damping: 20 }}
>
{children}
</motion.div>
``
The spring physics at stiffness: 400, damping: 20 gives you the bounce on hover without writing a single cubic-bezier value. That said, Framer Motion adds roughly 34 KB gzipped to your bundle — if you're optimising hard for Core Web Vitals, stick with the CSS approach. Both get you to the same visual place.
Responsive Clay Cards in a Grid Layout
Clay cards rarely live alone — you're usually building a grid of them for pricing, features, team members, or portfolios. The grid itself is straightforward, but you need to be careful about shadow clipping. A parent with overflow: hidden or a tight container will crop the outer box-shadow and your cards will look like they have shadows on some sides but not others.
``tsx
// ClayCardGrid.tsx
export function ClayCardGrid({ cards }: { cards: ClayCardData[] }) {
return (
<div
className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3"
style={{
gap: '2rem',
// padding on all sides so outer shadows aren't clipped
padding: '2rem',
// NEVER use overflow-hidden on this container
}}
>
{cards.map((card) => (
<ClayCard key={card.id} color={card.color}>
<h3 className="text-xl font-bold text-white mb-2">{card.title}</h3>
<p className="text-white/80 text-sm leading-relaxed">{card.body}</p>
</ClayCard>
))}
</div>
);
}
``
That padding: '2rem' on the grid container is non-negotiable. Box shadows render outside the element's bounding box. If the container has zero padding and a defined width, the 0 16px 40px outer shadow on cards at the edge of the grid will get cut off by the viewport or a parent container. Give the grid room to breathe.
For the gap between cards, 2rem is a good starting point but you can go up to 2.5rem if you have room. Clay cards with their elevated shadows need more breathing room than flat cards — the floating effect reads better when each card has clear space around it. Tight grids with 1rem gaps look cramped and the shadows start to visually merge.
Check out Empire UI's templates for full-page clay layouts including pricing grids, team grids, and feature sections — all with the shadow clipping issue already solved and responsive breakpoints baked in.
When to Reach for Claymorphism vs Other Card Styles
Claymorphism cards are a deliberate design statement. You'd use them for marketing landing pages, onboarding flows, children's apps, gaming dashboards, health and wellness UIs, or any context where personality and approachability outweigh density. The question you should ask is: does this UI need to feel fun? If yes, clay. If no, probably not.
Compare it to glassmorphism components — glass cards feel airy and futuristic, often used in dark-mode dashboards and data visualisation overlays. Glass is quieter. Clay is louder. You wouldn't put glass cards on a kids' learning platform, and you wouldn't put clay cards on a Bloomberg terminal. Context decides.
Neumorphism is the third comparison point. It extrudes elements from a monochromatic surface — very low contrast, very subtle. Neumorphism has documented accessibility problems with low-contrast shadows failing users with low vision. Claymorphism avoids that entirely because you're placing saturated colour against white or light grey, so contrast is structurally easier to achieve. If accessibility is a hard requirement (it should be), clay is the safer pick between the two.
One more thing — don't mix all three morphisms in the same page. Pick one as your primary card style. You can use a second style for accent elements sparingly, but three competing depth conventions in the same viewport creates visual chaos. Empire UI's style switcher makes it tempting to experiment — but commit to one for production. The box shadow generator can help you fine-tune your chosen style's shadow parameters before you build.
FAQ
You're almost certainly missing the inset highlights. A clay card needs at least four shadow layers — two inset (top white highlight, bottom dark anchor) and two outer (coloured lift, neutral base). Without the inset white at the top, you just have a rounded card with a drop shadow. Add inset 0 6px 12px rgba(255,255,255,0.50) and it'll click into place immediately.
It can, but it's harder. The coloured lift shadow disappears against dark backgrounds because there's not enough contrast between the shadow and the page. You'd need to increase shadow opacity significantly (0.7+) and often add a white border around the card to maintain the edge definition. Claymorphism really does read best over white or very light grey surfaces.
Wrap all transform and shadow transitions in a @media (prefers-reduced-motion: no-preference) block. Users who have reduced motion enabled in their OS will get a static card with no hover or press animation — which is fine, because the card still looks great with just the layered shadows and colour. The interaction is an enhancement, not a requirement.
Not fully — the multi-layer box-shadow stack isn't expressible with standard Tailwind utility classes alone. You'd need to extend your tailwind.config.js with a custom boxShadow key for your clay shadow values, then apply it as a single utility class. The CSS custom property approach shown in this article slots into Tailwind's [&]: arbitrary value syntax too, but a named config extension is cleaner for a component you'll reuse often.