Most 3D buttons on the web are a single element with a solid shadow underneath it. A zero-blur box-shadow acts as the bottom edge, and a downward translate on :active makes the face sink into it. Stacked layers and real 3D transforms fake the same depth with more markup, and all three versions are meant to sink when pressed.

Depth made from shadows has one known weak point, and it only shows up for some users. In forced colors mode, such as Windows High Contrast, browsers force box-shadow to none, so the raised edge disappears unless a border replaces it (MDN Web Docs, 2026).

How do you build a CSS 3D button with box-shadow?

See the Pen
3D Button with Shading (Experiment)
by Takane Ichinose (@takaneichinose)
on CodePen.

The fast version needs one element and two rules. The shadow is the bottom edge, and the :active state moves the button down onto it.

No JavaScript is involved. The face moves down while the edge stays where it is, and that reads as a physical key.

  1. Start with a plain button element. Remove the default border, then set padding (14px 36px), a 12px border-radius and a saturated background.
  2. Add the edge as a box-shadow with 0 blur and a 6px vertical offset, in a darker shade of the face color.
  3. Add the ground shadow as a second, blurred layer (0 12px 16px at 30% black) so the button separates from the page.
  4. Write the pressed state. On :active, translate the button down 4px and cut the edge offset from 6px to 2px.
  5. Transition transform and box-shadow over about 80ms so the press doesn’t snap.
.btn3d {
padding: 14px 36px;
border: none;
border-radius: 12px;
background: #e8344e;
color: #fff;
font-size: 1.125rem;
cursor: pointer;
box-shadow: 0 6px 0 #9c1f34, 0 12px 16px rgba(0, 0, 0, 0.3);
transition: transform 80ms, box-shadow 80ms;
}

.btn3d:active {
transform: translateY(4px);
box-shadow: 0 2px 0 #9c1f34, 0 4px 6px rgba(0, 0, 0, 0.3);
}

What sells the effect is shrinking the edge by exactly the translate distance. A 6px edge minus a 4px press leaves 2px, so the bottom of the shadow stays on the same pixel row while the face sinks into it.

The catch is cost. Josh W. Comeau’s 3D button tutorial (last updated May 2026) flags box-shadow and border as expensive properties to animate. The layered build later in this article moves elements instead for exactly that reason.

When I’m tuning the edge and ground layers, a box shadow generator saves a lot of reloading, since every offset change shows up live.

Comparing the depth techniques for a CSS 3D button

For most interfaces, stacked box-shadow is enough. Buttons that animate a lot do better with layered elements, and true 3D transforms only earn their complexity when the button actually rotates. Inset and outset borders are the simplest to write and the hardest to control.

TechniqueHow depth is builtPress behaviorMain limit
Inset and outset bordersBrowser-shaded border sidesSwap outset for insetLittle color control
Stacked box-shadowSolid zero-blur shadow edgeTranslate plus smaller shadowAnimates a costly property
Layered elementsShadow, edge and front spansTransform on front and shadowExtra markup, absolute layout
Perspective and preserve-3dFaces placed in real 3D spacerotateX or translateZFlattening properties break it

The deciding question is whether the button ever turns or flips. If it does, use true 3D. If it only animates on hover and press, layers hold up better, and for everything else a shadow edge is plenty.

On a primary call-to-action button I’d take the shadow edge over the fancier options nearly every time. It reads as depth at a glance. A rotating button adds a stacking context and flattening rules and doesn’t get any clearer for it.

Inset and outset borders

One declaration swaps a raised look for a pressed one, and the browser picks the shades automatically, as thesitewizard.com describes. Coloring the four borders by hand (light on top and left, dark on bottom and right) gives more control.

What is shaping UX design today?

Uncover the newest UX design statistics: user behavior, design trends, ROI data, and insights driving better digital experiences.

Check Them Out →

It needs no extra markup, no shadows and no transforms. The downside is that the browser decides how the sides are shaded, and the result looks dated in most modern interfaces.

Stacked box-shadow

See the Pen
3D buttons with Highlights and Shadows
by Justin Castaneda (@DagorathUryens)
on CodePen.

It’s one element, two rules and a few minutes of work.

The press transition animates box-shadow, which Comeau’s tutorial ranks as costly. On a single button nobody will notice. Across a dense grid of animated buttons it adds up.

Layered elements

Because only transforms move, animation stays cheap. The shadow gets its own blur, independent of the face, and gradient edges make rounded corners look lit.

The price is extra markup (three spans) and absolute positioning on the other layers, while the front layer has to stay in flow to size the button.

Perspective and preserve-3d

See the Pen
Perspective button hover effect
by Comehope (@comehope)
on CodePen.

This is the only option here with real depth that changes with the viewing angle, which is what you need for flips, tilts and cube-style reveals. It also brings flattening rules and a new stacking context (both covered in the true 3D section below), and a rotating button is harder to read at rest.

How to build a layered CSS 3D button

The layered version wraps three spans in one button element, called shadow, edge and front, and animates only the front and the shadow with transform. Josh W. Comeau’s tutorial, last updated May 2026, is the clearest published version of this structure.

What each layer does

The shadow layer is absolutely positioned at full size, filled with 25% black, softened with blur(4px) and nudged down with translateY.

The edge sits in the same spot, also absolutely positioned at full size, but uses a linear gradient that runs dark, base color, dark. That makes the rounded corners look like they catch less light.

The front is relatively positioned and holds the padding and text. It’s the only in-flow child, which is what gives the button its width and height.

DOM order handles the stacking, so no z-index is needed. Every layer shares the same 12px border-radius.

Offsets by state

StateFront layerShadow layer
ResttranslateY(-4px)translateY(2px)
HovertranslateY(-6px)translateY(4px)
PressedtranslateY(-2px)translateY(1px)

The shadow travels opposite to the front, so the gap between them widens on hover and closes on press. The pressed front stops 2px above the edge instead of 0, which keeps the 3D illusion alive mid-click.

If the button never animates, skip this build. All that extra markup buys you nothing.

Using perspective and rotateX for a true 3D button

True 3D buttons place their faces in actual 3D space. Perspective goes on the parent, transform-style: preserve-3d on the container, translateZ separates the faces and rotateX turns them. I’d reserve all of it for buttons that flip, tilt or reveal a back face.

How perspective length changes the look

The perspective property sets the distance between the viewer and the z=0 plane, so a large value gives a mild effect and a small value a strong one (MDN Web Docs, page updated April 2026). Values under 1px are clamped to 1px.

Any value other than none also creates a stacking context and turns the element into the containing block for fixed-position descendants. The property has been Baseline widely available since September 2015, and the guide to CSS perspective values goes deeper on tuning the length.

Where preserve-3d breaks

The transform-style property isn’t inherited, so every non-leaf descendant that holds 3D children needs it too.

Even then, MDN’s transform-style reference (updated July 2026) lists properties that force a flat result despite preserve-3d, including:

  • opacity below 1
  • overflow other than visible or clip
  • filter other than none
  • clip-path, mask-image or mix-blend-mode other than their defaults

That list catches a popular hover trick. Comeau’s layered button brightens with filter on the button itself, so copying that onto a preserve-3d container flattens the faces.

A flip button from three faces

See the Pen
3D flip button
by imvisrut (@imvisrut)
on CodePen.

hongkiat’s flip button tutorial uses a wrapper and a container with three face divs inside. There’s a front face, a back face and a middle face that fills the depth between them.

translateZ pushes the front and back faces apart along the z-axis, and backface-visibility set to hidden stops the reverse side of the front face from showing through. A rotateX on the container, triggered on hover, does the flip.

There’s one old caveat. A 2015 Creative Bloq tutorial reported jagged edges on 3D-transformed shapes in Firefox. Treat that note as dated and test current Firefox before shipping a rotated button.

Setting the hover, active and release states

The press should get the fastest transition and the return to rest the slowest, with the hover somewhere in between and a bit springy. That ordering is what makes the button read as a physical object instead of a fading rectangle.

How each state should behave

On hover, the front rises a few pixels and the button brightens. On press, the front drops toward the edge almost instantly. Release springs it back up with a small overshoot, and once the pointer leaves, it sinks back to rest slowly.

Treat each of these as its own micro-interaction with separate timing. One blanket transition flattens all four into the same motion.

Timing and easing values

ActionDurationEasing
Press34msBrowser default
Hover and release250mscubic-bezier(.3, .7, .4, 1.5)
Leave600mscubic-bezier(.3, .7, .4, 1)

Josh W. Comeau’s tutorial (last updated May 2026) sets the press at 34ms, roughly 2 frames at 60fps, because that is how quickly people push real buttons.

There’s a catch. Hover and release share a rule, so the rise-up and the snap-back after a click cannot get separate curves in pure CSS. Splitting them takes JavaScript.

A 2025 update note in the same tutorial adds that the linear() timing function can model actual spring physics, which Bezier curves only approximate. A CSS transition generator speeds up comparing curves before you commit to one.

Making a CSS 3D button accessible

The raised look turns into a barrier quickly when keyboard users can’t see focus, when the element isn’t a real button, when contrast and size fall short, or when the motion can’t be turned off.

Focus rings on a raised button

Comeau’s build keeps a 4px outline-offset and hides the ring only for pointer users with :focus:not(:focus-visible), so keyboard focus stays visible.

The offset pushes the ring off the button, so W3C’s Understanding document for SC 1.4.11 (updated September 2026) judges it against the page background, not the red face. In W3C’s examples a yellow ring on white fails and a dark green ring passes.

Removing the outline outright is a documented failure of success criteria 1.4.11, 2.4.7 and 2.4.13 (technique F78).

Button element or styled anchor

A button element brings the button role plus Enter and Space activation by default. An anchor activates on Enter only. Forced colors mode adds another reason to stick with native elements, since browsers pick system colors from native semantics and not from added roles (MDN Web Docs, 2026).

Use an anchor when the control goes to another URL, and a button for anything that performs an action.

Adding ARIA roles to an anchor or a div gives it the label without the behavior, and MDN notes that role button on a div will not pull ButtonText colors in forced colors mode.

Contrast, target size and reduced motion

A few numbers worth keeping nearby:

  • WCAG 2.2 SC 1.4.11 (Level AA) asks for at least 3:1 contrast on the visual information that identifies a control and its states.
  • SC 2.5.8 (Level AA) sets a minimum target size of 24 by 24 CSS pixels.
  • A 20 by 20 CSS pixel target passes only with 4 CSS pixels of spacing between targets, and fails with no gap (W3C example).
  • prefers-reduced-motion has been Baseline widely available since January 2020 (MDN Web Docs).

Run the face and edge colors through a color contrast check against the page background. Hover raises need no 3:1 of their own, but they must not make the button lose contrast.

The reduce value means the user wants motion removed, reduced or replaced, and MDN names scaling and panning of large objects as vestibular triggers. For this button, drop the transition and keep a cue that does not move, such as the brightness change.

@media (prefers-reduced-motion: reduce) {
.btn3d,
.front {
transition: none;
}
}

Does animating a CSS 3D button hurt performance?

On one button, no. On a grid of buttons animating blurred shadows, yes, because the browser repaints those frames while transform and opacity changes stay on the compositing stage. Default to the cheap properties and measure before adding anything else.

Cheap and costly properties

web.dev’s animation guide (last updated October 2020) demos the gap. Moving a box with top and left dropped 50% of frames, and the same move with transform dropped 1%.

The demo animates top and left, not box-shadow, so read the percentages as the shape of the gap and not as a shadow benchmark.

Transform and opacity are the cheap ones. Anything that triggers layout or paint costs more, and anything with a blur, a shadow included, paints slower than a flat box.

Where the sources disagree on will-change

Comeau’s final code declares will-change: transform on both moving layers from the start.

MDN and web.dev treat it as a last resort. MDN (page updated October 2026) warns that a stylesheet declaration keeps the browser’s rendering hints in memory far longer than needed, and that overuse can slow the page down.

The gap comes from scale. Comeau tunes one showpiece button, while MDN’s warning targets many elements carrying the hint at once.

Leave it out until DevTools shows dropped frames. If you add it, put it only on the moving layers.

Checking it in DevTools

  1. Record a hover and a press in the Chrome DevTools Performance panel, then read the Summary tab. A large Rendering value points to layout and style work, and a large Painting value points to repaint work.
  2. Turn on Paint flashing in the Rendering tab and press the button. Flashes outside the button mean extra repainting.
  3. Enable the FPS meter (Frame Rendering Stats in the Rendering tab) and watch the dropped-frames percentage while the animation runs.

Where CSS 3D buttons break or do not fit

Forced colors mode and touch screens are where these buttons break, and minimal interfaces are where they look wrong. They suit a few big actions, not every button on a page.

Forced colors mode

Windows High Contrast is the usual example. Under forced colors, browsers force box-shadow and text-shadow to none, and background images that are not url-based go too, which takes gradients with them (MDN Web Docs, April 2026). A shadow-based button loses its depth, and in the layered build the edge gradient disappears as well.

@media (forced-colors: active) {
.btn3d {
border: 2px solid ButtonText;
}
}

MDN’s own example does exactly this for a shadow-based button, and its guidance is to use forced-colors for small fixes, not a separate design.

The media feature has been Baseline widely available since September 2022, so the fix is safe to ship.

Touch screens and press feedback

MDN’s hover media feature reports none for devices that cannot hover or only emulate it on a long tap, so gate the raise behind @media (hover: hover).

Mobile browsers also flash a grey tap rectangle, which Comeau removes with -webkit-tap-highlight-color: transparent. On iOS, a long press can try to select the label after a second, and user-select: none stops it, though Comeau warns that properties like this should be used extremely rarely.

Finish with a real cross-browser compatibility pass. Comeau points to MDN’s notes on how browsers differ on whether a click focuses a button, and Safari needs Option+Tab to reach one.

Interfaces where it does not fit

Comeau calls his version ostentatious and says he would not use it on a GDPR cookie-consent banner. His rule is to keep it for grand, exciting actions.

Dense dashboards and forms with many actions get noise instead of clarity from raised buttons, and a page with ten of them has no primary action. A minimalist design system is the clearest case where a flat button wins.

CSS 3D Button Examples To Check Out

Subtle Glam by Robin

See the Pen
3D button
by Robin Delaporte (@robin-dela)
on CodePen.

A Button that’s Pure Art by Alex Zaworski

See the Pen
Single-element 3d button
by Alex Zaworski (@alexzaworski)
on CodePen.

Blendin’ 3D Modes Like a Boss by Lisi

See the Pen
CSS 3D Blend Mode Buttons
by Lisi (@lisilinhart)
on CodePen.

Jacob Beers’ Take on 3D

See the Pen
3d Button
by Jacob Beers (@jbeers)
on CodePen.

Sweet Candy Gradient Love

See the Pen
💐 Colorful Gradient Flex Buttons 💐
by 0guzhan (@0guzhan)
on CodePen.

The Skew Genius of Burmese Potato

See the Pen
Skew Button @aainterior
by Burmese Potato (@BurmesePotato)
on CodePen.

Scroll Magic with 3D by Jonas Sandstedt

See the Pen
3D buttons with scroll effect – Neumorphism
by Jonas Sandstedt (@sandstedt)
on CodePen.

Arcade Nostalgia Packed in a Button

See the Pen
Arcade Button
by Álvaro (@alvarotrigo)
on CodePen.

Takane Ichinose’s Hologram Spectacle

See the Pen
CSS Only “hologram” effect Button 3D Icon
by Takane Ichinose (@takaneichinose)
on CodePen.

That Toggle Round Button by Alvaro

See the Pen
Light power button
by Álvaro (@alvarotrigo)
on CodePen.

Ghosty to Glinty Magic by Jessica Biggs

G-Style 3D Masterpiece by Niklas

See the Pen
Google 3D Button
by Niklas (@nikname)
on CodePen.

Round, Checked, and Just CSS

See the Pen
3D Button
by Álvaro (@alvarotrigo)
on CodePen.

Go Loco with Comical 3D by Robert Miller

See the Pen
Button Drop Shadow
by Robert Miller (@iamrobertmiller)
on CodePen.

Toggle Fun in Round 3D

See the Pen
Toggle switch with checkbox:checked
by Álvaro (@alvarotrigo)
on CodePen.

Metallic Majesty by Comehope

See the Pen
Metallic glossy 3d button effects
by Comehope (@comehope)
on CodePen.

Circle of Minimalism by Alvaro

See the Pen
Minimal Circular 3D Buttons
by Álvaro (@alvarotrigo)
on CodePen.

A Bit of Chemistry with Ash Creator

See the Pen
ケミカルなボタン
by あしざわ – Webクリエイター (@ash_creator)
on CodePen.

Wibble Wobble, Old Meets New

See the Pen
3d Button
by Piet (@pietvanzoen)
on CodePen.

Drus Unlimited’s 3D Showstopper

See the Pen
3d button effect
by drus unlimited (@drus)
on CodePen.

The Big Jelly Shake by Arronhunt

See the Pen
BIG Jelly Button – original by madgraphism
by Arron Hunt (@arronhunt)
on CodePen.

Morphing Magic by Amit Sheen

See the Pen
Morphing Button
by Amit Sheen (@amit_sheen)
on CodePen.

Chic and Sleek Animated Goodness

Wobbly Wiggle Button Over Here

See the Pen
Wiggle Wiggle
by Gabriel Cyrillo (@gcyrillo)
on CodePen.

Journey Through Space with this Button

See the Pen
Hyperspace Button Effect
by Maurice Melchers (@mephysto)
on CodePen.


3D Button, Plus or Minus

See the Pen
3d Button with +/- transform
by Christos Hrousis (@christoshrousis)
on CodePen.

Going Old School with 3D Retro Buttons

See the Pen
3D Retro Buttons
by Mike @ Titan Global Tech (@b1tn3r)
on CodePen.

CSS3’s Gift: The Pseudo Button

See the Pen
3D CSS3 Button using :before & :after
by Simon Clavey (@simoncla)
on CodePen.

CSS 3D Button FAQ

How do I style a disabled CSS 3D button?

Flatten it. Scope the hover and press rules with :not(:disabled) so they never fire, then remove the lift and desaturate the face.

WCAG 2.2 SC 1.4.11 exempts inactive controls from the 3:1 contrast rule (W3C), but the label should stay readable.

Can I build a CSS 3D button in Tailwind CSS?

Yes. An arbitrary value such as shadow-[0_6px_0_#9c1f34] sets the solid edge, and active:translate-y-1 moves the button down 0.25rem on press.

Add a transition utility, then shrink the edge in an active: shadow variant. More patterns sit in these Tailwind button examples.

Does a Bootstrap button work as a base for this effect?

Yes. Add a custom class with the edge shadow and an :active translate, loaded after Bootstrap’s stylesheet so it wins the cascade.

Test keyboard focus afterward, because Bootstrap draws its focus ring with box-shadow and the new edge can replace it. The Bootstrap button component supplies the color variants.

What separates a CSS 3D button from a neumorphic button?

See the Pen
Skeumorphic Button
by Vlad Racoare (@vladracoare)
on CodePen.

A neumorphic button extrudes softly out of a background of the same color, using paired light and dark shadows. The 3D version is blunter, with a solid, darker edge under a face that clearly stands apart.

That solid edge keeps the control identifiable against the page, which matters for the 3:1 non-text contrast rule in WCAG 2.2 SC 1.4.11 (W3C).

Which edge and shadow colors work on a dark background?

See the Pen
Darkness
by Chance Squires (@chancesq)
on CodePen.

Lower the lightness of the face color to get the edge, as Josh W. Comeau’s build does (32% against 47%).

On dark pages a black ground shadow barely shows, so let the edge carry the depth.

Where does an icon or gradient go on the front layer?

See the Pen
3D Gradient Buttons
by Sietse (@firepenguin)
on CodePen.

Place the icon inside the front layer so it travels with the face, and set the gradient as that layer’s background.

Forced colors mode drops gradient background images (MDN Web Docs), so the label must stay readable on a plain background color.

Making a CSS 3D Button Reusable Across Sizes and Colors

Putting edge height, press distance and hue in custom properties makes the button reusable, because every size and color variant can derive its shadow values from those inputs.

Start with edge height, since every other offset depends on it. Press distance comes next and is capped at the edge height. Hue goes last, with the edge lightness shifted down from the face.

A press distance larger than the edge height pushes the shadow above the face, so the cap is a hard limit, not a style choice.

Timing belongs in shared tokens too. A 600ms return to rest against a 34ms press is a ratio of roughly 18 to 1, and a variant that overrides only one of the two will feel broken next to its siblings.

Readability takes a hit, though. A calc() inside box-shadow is harder to scan than a hardcoded value, so a one-off button should keep the plain rule.

Bogdan Sandu
Latest posts by Bogdan Sandu (see all)