3D Button
2025.05.14#showcaseClick on the button below to witness a very neat effect that makes the button feel like it goes into the page. Let's breakdown how it works:
HTML setup
One div needed, we will use the ::before
and ::after
pseudo-elements to create the 'sides' of the button
<button class="three-d">3D button</button>
CSS <button>
styling
The .three-d
class applied to <button/>
is defined as follows:
.three-d {
--size: 10px;
--negative-size: calc(-1 * var(--size));
--half-size: calc(var(--size) / 2);
--negative-half-size: calc(-1 * var(--half-size));
--quarter-size: calc(var(--size) / 4);
--deg: 45deg;
--teal: hsla(180, 40%, 50%, 1);
--plum: hsla(320, 45%, 45%, 1);
position: relative;
margin-top: 1rem;
color: white;
padding: 1rem 2rem;
background: linear-gradient(90deg, var(--teal) 0, var(--plum) 100%);
font-size: 2rem;
font-family: "Arial";
border: none;
}
--size
sets up a base value we can adjust later to programmatically modify downstream attributes using the value that help to keep our button looking 3D.--negative-size
,--half-size
,--negative-half-size
,--quarter-size
are pre calculated values to evenly adjust the overall button in attributes used on the pseudo elements.--deg
, will be for skewing the bottom and side elements of the 3D button.--teal
,--plum
are colors that have good contrast on black and white backgrounds we will use.position:relative
is important to control where the::before
,::after
elements will ultimately be placed.background: linear-gradient(...)
needs to start with the90deg
value so the gradient goes from left to right (default is top to bottom). Then we transition from the colorteal
toplum
.
Bottom side element setup
This sets up the bottom part of button for the 3D effect:
.three-d::before {
left: var(--half-size);
bottom: var(--negative-size);
width: 100%;
height: var(--size);
background-color: var(--teal);
transform: skewX(var(--deg));
}
Let's look at some of these calcuations.
transform: skewX(...)
creates the correct shape for a 3D effect on the bottom of the button. Skewing on the x-axis will change the left and right sides of the::before
element.left
calculation pushes the element to the right just enough so the top-left skewed corner touches the left side of the<button/>
element.bottom
would normally start the::before
element inside the<button/>
element, but the calculation resulting in a negative value shifts it down into place exactly at the bottom-left corner of the<button/>
element.
On the left below is the skewed shape in isolation and on the right the borders of the <button/>
element have been dotted in.
Right side element setup
.three-d::after {
top: var(--half-size);
right: var(--negative-size);
width: var(--size);
height: 100%;
background-color: var(--plum);
transform: skewY(var(--deg));
}
Similar calculations happen here but oriented to the right element. Below we have on the left the ::after
element in isolation and on the right borders dotted in.
Enabling the animation with transition
.three-d,
.three-d:before,
.three-d:after {
transition: all 0.2s;
}
No complex keyframe animations needed here since we are doing a simple not-pressed -> pressed state change on the button which is perfect for transition
. The 3 elements involved will work in unison and use all
attributes to simplify declarations needing to made for each animated piece. Animation duration is 0.2s
and by default our timing function is ease
.
When you actually click
To make this interactive we need to display motion during the ::active
pseudo-class state (a.k.a when the button is actively pressed) among all the elements <button/>
, ::after
, ::before
.three-d:active {
transform: translate(var(--half-size), var(--half-size));
}
.three-d:active:before {
left: var(--quarter-size);
bottom: var(--negative-half-size);
height: var(--half-size);
}
.three-d:active:after {
top: var(--quarter-size);
right: var(--negative-half-size);
width: var(--half-size);
}
Variable usage comes in to play heavily here to take out a lot of the potential calc
statement and keep things "DRY". These changes are what give the feeling that the button is being 'pressed' into the page. Play around with initial --size
variable to see the effect of things on the full demo on codpen.
~ Zano