Hung-Yi's LogoHung-Yi’s Journal

Pure CSS True 3D Button

Discover how to make a chunky, glowing, 3D button that animates on hover and click, with no JavaScript! Just clever use of HTML and 3D CSS transforms.

Sometimes you just need a big, shiny call-to-action that draws the user’s attention to the most important thing on a page, but you’ve already exhausted your UI framework’s colors, borders and button options. You want this thing to really pop.

…and what could pop more than a chunky, glowing, animated 3D button? 😎

Click me, I won't hurt you!

Feels rewarding to click, doesn’t it?

The CSS and HTML markup are provided below. The key parts to note are:

  1. CSS variables allowing reuse of repetitive properties (e.g. color)
  2. 3D transforms to move and rotate the bottom and side faces of the button into realistic positions
  3. perspective and perspective-origin on the whole button to reveal the bottom and side faces
  4. filter: brightness to darken the bottom and side faces and light up the front face on hover
  5. box-shadow for the glow effect
  6. transform: scale and transform-origin1 to change the sizes of the bottom and side faces on hover/click
  7. transform: translateZ to move the front face in and out of the “screen” on hover/click
.chunky-button {
  border: none;
  background: none;
  padding: 0;
  border-radius: 0;
  position: relative;
  perspective: 2000px;
  perspective-origin: 1200px 1000px;
  transform-style: preserve-3d;
  cursor: default;
  font-size: 0.8rem;
  user-select: none;

  --color: #46e4bc;
  --transition: transform 0.1s ease, box-shadow 0.1s ease;
  --virtual-height: 10px;
  --factor: 0.38;

.chunky-button .front {
  background-color: var(--color);
  color: rgba(0, 0, 0, 0.7);
  font-weight: 600;
  text-transform: uppercase;
  letter-spacing: 0.05em;
  padding: 1rem 2rem;
  transition: var(--transition);
  border: 1px solid var(--color);
  backface-visibility: hidden;
.chunky-button:hover .front {
  transform: translateZ(calc(var(--virtual-height)*var(--factor)));
  box-shadow: 0 0 3rem var(--color);
  filter: brightness(1.1);
.chunky-button:active .front {
  transform: translateZ(calc(-1*var(--virtual-height)*var(--factor)));

.chunky-button .bottom {
  background-color: var(--color);
  filter: brightness(0.8);
  height: var(--virtual-height);
  width: 100%;
  position: absolute;
  left: 0;
  bottom: 0;
  transform-origin: bottom;
  --initial-position: rotateX(-90deg) translateY(100%);
  transform: var(--initial-position);
  transition: var(--transition);
.chunky-button:hover .bottom {
  transform: var(--initial-position) scaleY(calc(1 + var(--factor)));
.chunky-button:active .bottom {
  transform: var(--initial-position) scaleY(calc(1 - var(--factor)));

.chunky-button .side {
  background-color: var(--color);
  filter: brightness(0.9);
  width: var(--virtual-height);
  height: 100%;
  position: absolute;
  top: 0;
  right: 0;
  transform-origin: right;
  --initial-position: rotateY(90deg) translateX(100%);
  transform: var(--initial-position);
  transition: var(--transition);
.chunky-button:hover .side {
  transform: var(--initial-position) scaleX(calc(1 + var(--factor)));
.chunky-button:active .side {
  transform: var(--initial-position) scaleX(calc(1 - var(--factor)));

Then place this HTML where you want the marker to appear.

<div class="chunky-button">
  <div class="bottom"></div>
  <div class="side"></div>
  <div class="front">
    Click me, I won't hurt you!



I went through many iterations of janky and crunchy animations before figuring out that transform-origin was the best approach to anchoring the bottom and side faces of the button “in place” while they changed sizes. It’s important to use the simplest transform changes possible on hover/click to keep the animation clean.