Originally published on Crafttor.
TL;DR: A good button hover animation in CSS is short, eased and honest about what the button does. Use 150 to 300 ms, an ease-out curve, and animate only transform, opacity and colour where you can. Below are four effects with code and the timing that makes them feel right. You also get a reduced-motion fallback and a faster way to copy a hover you like.
Buttons are the most hovered element on any page. A small, well-timed response tells the user "this is clickable, and it is ready". A slow or bouncy one feels laggy. The difference is rarely the effect itself. It is almost always the duration and the easing.
Start with a solid base button
Every effect below builds on the same base. Keep the transition on the base rule, not the :hover rule, so it plays both ways.
.btn { position: relative; display: inline-flex; align-items: center; gap: 8px; padding: 12px 20px; border: 0; border-radius: 10px; background: #17171c; color: #fff; font: 600 15px/1 system-ui, sans-serif; cursor: pointer; transition: transform 200ms cubic-bezier(.22, 1, .36, 1), box-shadow 200ms cubic-bezier(.22, 1, .36, 1), background-color 200ms ease-out; } List each property in transition. Writing transition: all is tempting, but it animates things you did not plan, like a width change from a font swap.
Four button hover animation CSS effects
Each effect suits a different job. Pick one per button style, not all four at once.
1. Fill sweep
A colour fills the button from one side. It reads as "you are about to commit to this", which suits primary actions.
.btn-sweep { overflow: hidden; isolation: isolate; } .btn-sweep::before { content: ""; position: absolute; inset: 0; z-index: -1; background: #fb9f3d; transform: scaleX(0); transform-origin: left; transition: transform 260ms cubic-bezier(.22, 1, .36, 1); } .btn-sweep:hover::before { transform: scaleX(1); } Animating scaleX on a pseudo-element is cheaper than animating width or background-position. The browser can run it on the compositor. If the text colour changes too, check contrast on both the start and end colours.
2. Lift and shadow
The button rises a few pixels and its shadow grows. It feels physical, and it works well on cards and secondary buttons.
.btn-lift:hover { transform: translateY(-2px); box-shadow: 0 8px 20px rgb(0 0 0 / .18); } .btn-lift:active { transform: translateY(0); box-shadow: 0 2px 6px rgb(0 0 0 / .18); transition-duration: 80ms; } Keep the lift small: 1 to 3 px. Larger lifts make a row of buttons look jumpy. The :active rule gives a quick press back down, which closes the loop.
Animating box-shadow repaints on every frame. On one button it is fine. On a grid of fifty cards, put the larger shadow on a pseudo-element and fade its opacity instead.
3. Arrow nudge
An arrow icon slides a few pixels in the direction of travel. It is the quietest of the four and suits links like "Read more" or "Next".
.btn-arrow svg { transition: transform 180ms ease-out; } .btn-arrow:hover svg { transform: translateX(4px); } Put the :hover on the button, not on the icon. The button is the bigger target, and the user hovers it first. This is the "hover the container, move the child" pattern.
4. Glow
A soft, coloured halo appears behind the button. It works on dark backgrounds and for one hero call to action per page.
.btn-glow::after { content: ""; position: absolute; inset: -6px; z-index: -1; border-radius: 14px; background: #fb9f3d; filter: blur(14px); opacity: 0; transition: opacity 240ms ease-out; } .btn-glow:hover::after { opacity: .55; } The blur is set once and never animated. Only opacity changes, which keeps it smooth. Animating filter: blur() directly is much heavier.
Timing and easing: the part that makes it feel right
Most hover problems are timing problems. Use this table as a starting point.
| Effect | Duration | Easing | Notes |
|---|---|---|---|
| Fill sweep | 220 to 300 ms | cubic-bezier(.22, 1, .36, 1) | Larger area, so slightly longer |
| Lift and shadow | 150 to 220 ms | ease-out | Press back in about 80 ms |
| Arrow nudge | 150 to 200 ms | ease-out | Small distance, short time |
| Glow | 200 to 260 ms | ease-out | Fade only |
A few rules hold across all four:
- Stay between 150 and 300 ms. Under 100 ms, the user barely sees it. Over 300 ms, the button feels slow to respond.
- Use ease-out for entering. The motion starts fast and settles, so the button answers the pointer right away.
- Leave faster than you enter. A hover-out at about two thirds of the hover-in time feels tidy. Put a second
transitionon the:hoverrule to make them differ. - Avoid bouncy springs on buttons. Overshoot is fun on toggles and tabs. On a button you hover many times a day, it gets tiring.
To make the exit faster than the entry:
.btn { transition: transform 140ms ease-in; } /* leaving */ .btn:hover { transition: transform 220ms ease-out; } /* entering */ The rule that applies is the one for the state you are moving into. That is why the faster value sits on the base rule.
If you want to go deeper on curves, our guide to CSS easing functions explained covers cubic-bezier() and steps() with examples.
Respect reduced motion and touch screens
Some users ask their system for less motion. Honour that with a media query. Keep the colour change, drop the movement.
@media (prefers-reduced-motion: reduce) { .btn, .btn::before, .btn::after, .btn svg { transition-property: opacity, background-color, color; } .btn-lift:hover, .btn-arrow:hover svg { transform: none; } } On phones, a tap can leave a hover style stuck until the user taps elsewhere. Wrap hover-only motion in a hover media query:
@media (hover: hover) and (pointer: fine) { .btn-lift:hover { transform: translateY(-2px); } } Two more checks before you ship:
- Give
:focus-visiblethe same visual change as:hover, so keyboard users get the same feedback. - Never rely on motion alone to show state. A disabled button should look disabled even with no animation.
Copy a hover you like instead of guessing the values
You often see a button hover on a real site and want the same feel. Guessing is slow. You eyeball 200 ms, it turns out to be 320 ms, and the easing was a custom curve you would never have picked.
DevTools can help when the hover is a plain CSS transition. Force :hover in the Styles pane and read the rule. It gets harder when the hover is started by JavaScript, uses GSAP, or animates several layers with different delays. Our walkthrough on how to capture hover animations covers each case.
Capture Motion reads the real values for you:
- Open the page in Chrome and start the Capture Motion extension.
- Pick the button, or its container if an inner icon moves too.
- Play the hover. The extension triggers a real
:hoverstate through the Chrome DevTools Protocol, attached only during the capture. - Preview the capture. You see each animated property with its keyframes, duration, delay and easing.
- Export it. With a free account, copy the CSS, Framer Motion or GSAP code, or send it to Figma as a Figma Motion timeline.
It records CSS transitions and keyframes, the Web Animations API, GSAP and Lottie. That covers the cases where DevTools shows no timing to read. Capture or rebuild only designs you own or have permission to use. Take the timing and easing, then apply them to your own button.
A quick checklist before you ship
- [ ] Duration between 150 and 300 ms
- [ ] Ease-out on enter, slightly faster exit
- [ ] Only
transform,opacityand colours animate - [ ]
:focus-visiblematches:hover - [ ] Reduced-motion fallback in place
- [ ] Hover motion wrapped in
@media (hover: hover) - [ ] Contrast checked at both ends of a colour change
Want a ready example with stagger and hover? See the animated avatar stack build, or browse the gallery on capturemotion.dev.
FAQ
What is a good duration for a button hover animation in CSS?
Between 150 and 300 ms covers almost every case. Small moves like an arrow nudge sit near 150 to 200 ms. Larger fills can run to 300 ms. Past that, the button starts to feel slow.
Which easing should I use for button hovers?
Use ease-out or a curve like cubic-bezier(.22, 1, .36, 1) for the hover-in. It responds instantly and settles gently. For the hover-out, a slightly shorter ease-in or ease-out works well.
Why does my hover animation stutter?
You are probably animating layout or paint properties like width, top or filter. Switch to transform and opacity. For shadows and glows, animate the opacity of a pseudo-element that already has the shadow.
How do I make hover-out faster than hover-in?
Put the faster transition on the base rule and the slower one on the :hover rule. The browser uses the transition of the state the element is moving into.
Can I copy the exact hover timing from another website?
Yes. For plain CSS, force :hover in Chrome DevTools and read the rule. For JavaScript or GSAP hovers, a capture tool like Capture Motion records the real keyframes, durations and easing. Only copy from sites you own or have permission to use.


