Skip to content

3D Button

2025.05.14#showcase

Click 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

html
<button class="three-d">3D button</button>

CSS <button> styling

The .three-d class applied to <button/> is defined as follows:

css
.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 the 90deg value so the gradient goes from left to right (default is top to bottom). Then we transition from the color teal to plum.

Bottom side element setup

This sets up the bottom part of button for the 3D effect:

css
.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

css
.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

css
.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

css
.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