Neumorphism. Very polarizing. But very satisfying.

Go ahead, touch this button.

Nice.

Whether you like it or not, here’s how to animate a neumorphic button with CSS only™.

Neumorphism breakdown

In CSS, neumorphism leans on two box shadows. I’ll call them the raised shadow and the pressed shadow.

css
/* raised */
box-shadow: 20px 20px 60px #c8c8c8, 
           -20px -20px 60px #ffffff;

/* pressed */
box-shadow: inset 20px 20px 60px #c8c8c8, 
            inset -20px -20px 60px #ffffff;

They’re really the same shadow, one of them is just inset. The first provides an element with the appearance of being raised off the page. The second makes things look pressed into the page.

BUT, you can’t animate between a normal and inset shadow.

The animation

Instead, apply both shadows to the target element, one on the :before pseudo-element, and one on the :after. Then animate the opacity of these pseudo-elements.

The key to realism is to first transition to no box shadow. When you press a button in real life, the button becomes flush with the surface before it is pressed into it.

0%
50%
100%

This transition keyframe can be achieved via a transition-delay.

Here’s the full CSS.

css
body {
  background: #ebebeb;
  --transition-time: 0.07s;
  --border-radius: 10px;
  --b-s: 6px; /* Box shadow */
  --shadow: calc(-1 * var(--b-s)) var(--b-s) calc(2 * var(--b-s)) #d1d1d1;
  --light: var(--b-s) calc(-1 * var(--b-s)) calc(2 * var(--b-s)) #ffffff;
}

button {
  position: relative;
  padding: 18px;
  background: transparent;
  border: 2px solid transparent;
  /* Neumorphic */
  border-radius: var(--border-radius);
  background: #ebebeb;
}
button:focus {
  outline: none;
  /* Keep buttons accessible! */
  border: 2px solid #bbdefb !important;
}
button::before, button::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  /* Neumorphic */
  border-radius: var(--border-radius);
  transition: opacity calc(var(--transition-time) / 2) linear;
}
button::before {
  box-shadow: var(--shadow), var(--light);
  opacity: 1;
  transition-delay: calc(var(--transition-time) / 2);
}
button::after {
  box-shadow: inset var(--shadow), inset var(--light);
  opacity: 0;
  transition-delay: 0s;
}
button:active::before {
  opacity: 0;
  transition-delay: 0s;
}
button:active::after {
  opacity: 1;
  transition-delay: calc(var(--transition-time) / 2);
}

For extra realism, transform the scale of the button’s content to 0.97 while the button is active. This enhances the 3D effect by making it seem like the text of the button is farther from you when pressed.

Try it yourself

Here’s a Glitch project with this code that you can edit: neumorphism.glitch.me