How to Build a CSS Button That Actually Looks Pressable
A practical guide to designing a CSS button: padding, radius, shadow, hover and active states, gradients, and WCAG contrast — with copy-paste CSS you can ship.
How to Build a CSS Button That Actually Looks Pressable
Most buttons on the web are rectangles that change color on hover and call it a day. The good ones feel like objects: they lift when your cursor arrives, they sink when you click, and the label stays readable the whole time. That difference is almost entirely a few CSS properties — padding, border-radius, box-shadow, a transform on :active, and an honest contrast ratio. This guide walks through each one and shows the exact CSS you end up with.
If you want to skip the hand-math, the CSS Button Generator gives you a live preview you can hover and press, then exports the coordinated :hover and :active rules. But it helps to understand what those rules are doing, so let's build one from the parts up.
Start With Padding and Radius
Padding sets the click target. A button with padding: 6px 12px is technically fine, but it reads as cramped and fails comfortable touch sizing on phones. A solid default is padding: 12px 24px — vertical breathing room plus generous horizontal space so the label never touches the edge.
Border-radius sets the personality. Square corners (border-radius: 4px) feel utilitarian and enterprise; a pill (border-radius: 999px) feels friendly and consumer; somewhere around border-radius: 12px is the modern middle. The radius should scale with the button, not stay fixed — a tiny icon button with a 12px radius looks almost square, while the same value on a tall CTA looks intentional. When I'm tuning a set of buttons I lock the radius first and let everything else follow it, because a mismatched corner is the fastest way to make a UI look stitched together. If you only want to dial in the corner shape, the border radius generator lets you preview asymmetric corners too.
Here's a clean base before any depth:
.btn {
padding: 12px 24px;
border: none;
border-radius: 12px;
font: 600 16px/1 system-ui, sans-serif;
color: #ffffff;
background: #4f46e5;
cursor: pointer;
transition: transform .12s ease, box-shadow .12s ease, filter .12s ease;
}
Note the transition line up front. Every state change below leans on it, so declaring it once on the base keeps :hover and :active from each needing their own.
Shadow: The Difference Between Flat and Physical
A single soft drop shadow makes a button float. Two layered shadows make it look like a real object. The trick for a 3D raised button is a solid-color shadow with zero blur, offset straight down — that becomes the visible side wall of the button:
.btn-3d {
box-shadow: 0 6px 0 #3b35b3, 0 8px 16px rgba(0,0,0,.28);
}
The first layer (0 6px 0 #3b35b3) is the wall. Its color is the background darkened by roughly 22%, so it reads like the lit top face casting onto its own edge. The second layer is a soft ambient shadow that grounds the button against the page. According to MDN's box-shadow reference, you can stack any number of comma-separated shadow layers, and they paint back-to-front — which is exactly why the sharp wall sits in front of the soft ground shadow here.
If you'd rather explore multi-layer shadows on their own — inset, multiple offsets, colored glows — the box shadow generator is built for that exploration before you commit a value.
Hover and Active: Making It Move
Hover is the easy half. Lift the button a few pixels and grow the wall so it reads as standing taller:
.btn-3d:hover {
transform: translateY(-2px);
box-shadow: 0 8px 0 #3b35b3, 0 12px 20px rgba(0,0,0,.30);
}
The press is where most hand-written buttons fall apart. On :active, the top face should move down by almost exactly the wall height, so it lands flush where the wall used to be:
.btn-3d:active {
transform: translateY(5px);
box-shadow: 0 1px 0 #3b35b3, 0 2px 6px rgba(0,0,0,.30);
}
The wall collapses from 6px to 1px while the face drops 5px. Your eye reads that as the button being pushed into the page. The single most common mistake here is mismatching those numbers — if the button sinks 10px but the wall was only 6px, the face overshoots and the click looks like a glitch rather than a press. Pair them: translateY should be the depth minus one.
Worked example. Say you start with depth 6 and a #4f46e5 fill. The export you'd ship looks like the three blocks above combined: a base with the dual shadow, a :hover that lifts 2px and grows the wall to 8px, and an :active that drops 5px and collapses the wall to 1px. Paste those three rules and you have a button that lifts on hover and depresses on click — no JavaScript, no extra markup.
Don't Forget Contrast and Focus
A pressable button that nobody can read is a failure, and a colorful one is the easiest place to fail. WCAG asks for a contrast ratio of at least 4.5:1 between the label text and its background for normal-size text. White-on-#4f46e5 clears it comfortably (around 8:1); white-on-#f59e0b (a cheerful amber) does not — it lands near 2:1 and needs dark text instead. Soft, low-contrast styles like neumorphism almost always fail, so check the readout before you hand the button to a developer.
The other half of accessibility is the focus state. When you set border: none and rely on hover for feedback, keyboard users get nothing — they tab to the button and see no indication it's selected. Always restore a visible focus ring:
.btn:focus-visible {
outline: 3px solid #a5b4fc;
outline-offset: 2px;
}
:focus-visible shows the ring for keyboard navigation but not for mouse clicks, so you keep the clean look without abandoning keyboard users.
Gradients and Glass for the Finishing Touch
Once the mechanics work, the surface is where you spend personality. A diagonal gradient fill turns a plain button into a hero CTA:
.btn-gradient {
background: linear-gradient(135deg, #6366f1, #ec4899);
}
Keep gradients subtle — two adjacent hues read as premium, while two opposite hues read as a warning sign. The gradient generator lets you tune the angle and stops with a live swatch.
Glass buttons over a hero image lean on backdrop-filter:
.btn-glass {
background: rgba(255,255,255,.18);
border: 1px solid rgba(255,255,255,.4);
-webkit-backdrop-filter: blur(12px);
backdrop-filter: blur(12px);
}
Ship both the prefixed and unprefixed property for Safari, and keep a solid fallback underneath for browsers that ignore backdrop-filter entirely — older engines simply render the translucent fill with no blur, which is a graceful degrade rather than a break.
Wrapping Up
A button that feels pressable is a stack of small, deliberate choices: comfortable padding, a radius that matches its scale, a two-layer shadow for physicality, paired hover and active transforms, a contrast ratio above 4.5:1, and a focus ring for keyboard users. None of it requires JavaScript, and all of it fits in a handful of CSS rules. Tune the values until the preview presses the way you want, copy the multi-state CSS, and ship it — that's the whole job.
Made by Toolora · Updated 2026-06-13