GODRICH
#css-animation#button#loader#transition#scroll#dashboard#ui#design-trends

9 CSS Button Animations (SCSS + React, copy-paste)

A CSS button animation is a small motion that tells the user a button noticed them: hover, press, wait, fail, or confirm.

Auto-plays · click a tile to jump to its section · all nine in one zip

The order isn't "most popular first." It's the order a button lives through: it has to get noticed (01 through 05), respond to a press (06 and 07), admit a failure (08), and ask twice before doing something dangerous (09). Try them in the grid above; each section shows only the core lines, and the complete files are in the zip. If the button's look matters more right now than its motion, the css button design set is the page to open first, and when the wait in 06 grows into a whole screen, the CSS skeleton loading screens carry it from there.

01Ripple

What you see

A circular wave spreads out from the click point and fades.

Ripple
Open in a new tab

01 · Ripple — Measured area 9.62% · moving frames 6/23Original by GODRICH · SCSS and React ship in the zip · See the password

600ms느리게 900ms어두운 물결

Where it fits

Use it on pay and confirm buttons, where the press has to feel like it landed.

.btn {
  position: relative; overflow: hidden; isolation: isolate;
  &::after {
    content: ""; position: absolute;
    left: var(--x, 50%); top: var(--y, 50%);
    width: var(--d, 0); height: var(--d, 0);
    border-radius: 50%; background: $ripple;
    transform: translate(-50%, -50%) scale(0); opacity: 0;
  }
  &.is-rippling::after { animation: ripple $duration $easing forwards; }
}
@keyframes ripple { to { transform: translate(-50%, -50%) scale(1); opacity: 0; } }

02Lift

The motion

The button rises 6px on hover, and its shadow grows.

Lift
Open in a new tab

02 · Lift — Measured area 6.583% · moving frames 13/23Original by GODRICH · SCSS and React ship in the zip · See the password

6px높게 10px그림자 없음

Where a css button hover animation belongs

It suits card-style buttons and add-to-cart buttons, anywhere the button should feel like something you can pick up.

$lift: 6px; $duration: 220ms; $shadow: rgba(23, 20, 26, .32);
.btn {
  will-change: transform;
  transition: transform $duration cubic-bezier(.2, .8, .2, 1), box-shadow $duration;
  &:hover, &:focus-visible { transform: translateY(-$lift); box-shadow: 0 14px 24px $shadow; }
  &:active { transform: translateY(-2px); box-shadow: 0 6px 12px $shadow; }
}

03Underline slide

What it does

The underline draws in from the left and, when the cursor leaves, slides out to the right.

Underline slide
Open in a new tab

03 · Underline slide — Measured area 0.339% · moving frames 16/23Original by GODRICH · SCSS and React ship in the zip · See the password

2px굵게 3px가운데서

When to use it

Use it for text links and navigation.

.link::after {
  content: ""; position: absolute; left: 0; right: 0; bottom: 0;
  height: $thickness; background: currentColor;
  transform: scaleX(0); transform-origin: right;
  transition: transform $duration cubic-bezier(.2, .8, .2, 1);
}
.link:hover::after { transform: scaleX(1); transform-origin: left; }

04Magnetic

What you see

When the cursor comes within a set radius, the button drifts toward it.

Magnetic
Open in a new tab

04 · Magnetic — Measured area 9.387% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · See the password

0.35강하게 0.6반경 120px

Where it fits

Use it on a landing page's single call to action; two of these on one screen turn the page into a toy.

document.addEventListener('pointermove', function (e) {
  var r = btn.getBoundingClientRect();
  var dx = e.clientX - (r.left + r.width / 2), dy = e.clientY - (r.top + r.height / 2);
  if (Math.hypot(dx, dy) < radius) btn.style.transform = 'translate(' + dx * strength + 'px,' + dy * strength + 'px)';
  else btn.style.transform = '';
});

05Rotating border

The motion

A gradient border spins continuously around the button.

Rotating border
Open in a new tab

05 · Rotating border — Measured area 1.17% · moving frames 17/23Original by GODRICH · SCSS and React ship in the zip · See the password

3s빠르게 1.5s2색

Good places for it

Use it on the one button that has to look special, such as an AI or premium feature.

.btn {
  position: relative; overflow: hidden; isolation: isolate;
  padding: $border; border-radius: 16px;
  &::before {
    content: ""; position: absolute; inset: -60%;
    background: conic-gradient(from 0deg, $colors);
    animation: spin $duration linear infinite; z-index: -1;
  }
}
.btn__in { display: block; border-radius: 13px; background: #17141a; }
@keyframes spin { to { transform: rotate(360deg); } }

06Loading state

What it does

On press, the label fades out and a spinner fades in.

Loading state
Open in a new tab

06 · Loading state — Measured area 1.013% · moving frames 15/23Original by GODRICH · SCSS and React ship in the zip · See the password

300ms폭 유지체크로 끝

When to use it

Use it for save and submit buttons, and keep the width fixed so the elements next to it don't shift.

.btn { display: grid; place-items: center; min-width: 200px; }
.btn__label, .btn__spin { grid-area: 1 / 1; transition: opacity $duration, transform $duration; }
.btn__spin {
  width: 22px; height: 22px; border-radius: 50%;
  border: 3px solid rgba(255, 255, 255, .35); border-top-color: #fff;
  opacity: 0; transform: scale(.6); animation: spin $spin linear infinite;
}
.btn.is-loading .btn__label { opacity: 0; transform: translateY(6px); }
.btn.is-loading .btn__spin  { opacity: 1; transform: scale(1); }

07Arrow push

What you see

On hover, the arrow line stretches and the head pushes to the right.

Arrow push
Open in a new tab

07 · Arrow push — Measured area 0.186% · moving frames 14/23Original by GODRICH · SCSS and React ship in the zip · See the password

8px멀리 14px선 없이

Where it fits

Use it for next, more, and continue; the arrow is just one line and one rotated corner, so no icon font is needed.

.arrow__line { transform-origin: left; transform: scaleX(.55); transition: transform $duration; }
.arrow__head {
  width: 12px; height: 12px;
  border-top: 2.5px solid currentColor; border-right: 2.5px solid currentColor;
  transform: rotate(45deg); transition: transform $duration;
}
.btn:hover .arrow__line { transform: scaleX(1); }
.btn:hover .arrow__head { transform: rotate(45deg) translate($push * .7, -$push * .7); }

08Shake

The motion

On error, the button shakes side to side three times and stops.

Shake
Open in a new tab

08 · Shake — Measured area 2.381% · moving frames 6/23Original by GODRICH · SCSS and React ship in the zip · See the password

6px세게 10px두 번만

Good places for it

Use it for a failed login or invalid input; anything more than five shakes reads as angry.

.btn.is-shaking { animation: shake $duration cubic-bezier(.36, .07, .19, .97) both; }
@keyframes shake {
  10%, 90% { transform: translateX(-$amount * .5); }
  20%, 80% { transform: translateX($amount); }
  30%, 70% { transform: translateX(-$amount); }
  40%, 60% { transform: translateX($amount * .7); }
  50%      { transform: translateX(-$amount * .7); }
}

09Hold to confirm

What it does

The background fills while you hold the button, and the action fires once it's full; release early and it resets.

Hold to confirm
Open in a new tab

09 · Hold to confirm — Measured area 8.162% · moving frames 18/23Original by GODRICH · SCSS and React ship in the zip · See the password

1.2s빠르게 0.8s세로로

When to use it

Use it for delete or unsubscribe instead of a confirmation modal. JS adds is-holding on pointerdown and removes it on pointerup, and the fill animation's animationend event is the signal to fire.

.btn__fill {
  position: absolute; inset: 0; background: $fill;
  transform: scaleX(0); transform-origin: left; z-index: -1;
  transition: transform 200ms ease-out;
}
.btn.is-holding .btn__fill { animation: fill $hold linear forwards; }
@keyframes fill { from { transform: scaleX(0); } to { transform: scaleX(1); } }

Where it breaks: three traps

Why only transform and opacity

All nine animate only transform and opacity, and that's deliberate. Animating width or top forces the browser to recompute layout every frame, while transform and opacity are handled entirely in the compositing step. MDN's animation performance guide recommends those two properties for exactly this reason. The tempting mistake is in 06, where a css button loading animation shrinks the button while it waits: that breaks the rule and shoves the elements next to it around. Keep the width fixed and change only what's inside.

Trap two — size

The second trap is size. The first time I built 01, I set a 320px ripple on a small button, and the circle covered the whole button in two frames, so it read as a quick flash instead of a wave. Compute the ripple diameter as twice the button's longer side, and don't go below 600ms. Those values are already baked into the nine files, and the archive password is xu8ewsq8 if you want them now.

Trap three — touch

The third trap is touch. 02, 03, and 07 depend on hover, which doesn't exist on phones. The files in the zip attach the same motion to :focus-visible, and on touch devices the :active state stands in for hover. Once more of your buttons have to say "still waiting" the way 06's ring does, the AI chat loading animations treat that whole waiting stretch as the subject rather than a button state.

Accessibility

Reduced motion

All nine, down to the css button click animation in 01, handle prefers-reduced-motion: reduce. The rule is to remove the motion but keep the feedback: 02 keeps the shadow without lifting, 06 shows a static ring, and 08 changes color instead of shaking. 09 is the exception: holding to confirm is a safety mechanism rather than decoration, so the fill stays. Browser support for the media query is listed on MDN prefers-reduced-motion.

@media (prefers-reduced-motion: reduce) {
  .btn.is-demo, .btn.is-shaking { animation: none; }
  .btn:hover { transform: none; }
}

More button and loader collections live in the CSS animations category, and you can read what this site is about on the about page.

FAQ

Can I use all nine css button animation examples in one project?

You can, but keep it to two kinds per screen. 04 (magnetic) and 05 (rotating border) are "one special button" effects, so use each at most once per page. The rest are tied to specific situations, so they rarely collide. If you want one more standout control on the same screen, a state switch like the dark mode toggle animations reads differently enough from 04 and 05 to sit beside them.

What replaces class toggling in React?

The components in the zip's react/ folder take state as props: loading for 06, a shakeKey failure counter for 08, and an onConfirm callback for 09. The SCSS modules only read var() values, so you pass the three variables in as props.

The animation looks janky. What do I check first?

First, make sure nothing other than transform and opacity is animating. Then add will-change: transform to the moving element only; putting it on everything makes things slower, not faster.

Enter the archive password

The password is inside this article. You will find it as you read.