How to Create Animated Buttons in CSS
This recipe builds a button with a smooth hover and click transition driven by transform and opacity, and explains why those two properties animate more smoothly than most others.
Why transform and opacity animate smoothly
Every property a browser can animate falls into one of a few performance tiers, and the difference comes down to what the browser has to redo on every single frame. Changing width, height, top, left, or margin changes the geometry of the box, so the browser has to re-run layout — recalculating the position and size of that element and potentially everything around it — and then repaint the affected area, sixty times a second if the animation is smooth. transform and opacity, by contrast, don't change an element's geometry at all: the element keeps its original size and position in the layout, and the browser just shifts or fades the already-painted result on the compositor, a step that can run on the GPU independently of the page's layout and paint pipeline. That's why a transform: scale() or translate() animation stays smooth even on a busy page, while animating width or top is far more likely to visibly stutter.
- Cheap to animate (compositor-only): transform (translate, scale, rotate, skew), opacity
- Expensive (triggers layout): width, height, top / left / right / bottom, margin, padding
- Expensive (triggers paint, not layout): background-color, box-shadow, border-color
Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Animated Button: Scale Effect</title>
<style>
body {
font-family: system-ui, sans-serif;
padding: 60px;
background: #f5f5f7;
}
.btn {
padding: 12px 26px;
font-size: 16px;
font-weight: 600;
color: #fff;
background: #6f42c1;
border: none;
border-radius: 10px;
cursor: pointer;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.15);
transition: transform 0.18s ease-out, box-shadow 0.18s ease-out;
}
.btn:hover {
transform: scale(1.06);
box-shadow: 0 6px 14px rgba(0, 0, 0, 0.2);
}
.btn:active {
transform: scale(0.96);
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.15);
}
@media (prefers-reduced-motion: reduce) {
.btn {
transition: none;
}
}
</style>
</head>
<body>
<button class="btn" type="button">Hover or click me</button>
</body>
</html>The transition property is shorthand for up to four values: the property to watch, the duration, the timing function, and an optional delay. For a hover-in effect, an "ease-out" curve (fast start, slow finish) tends to feel snappier and more responsive than the default ease, because the button appears to react instantly and then settle, rather than easing gradually into the change.
Example
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Animated Button: Sliding Fill</title>
<style>
body {
font-family: system-ui, sans-serif;
padding: 60px;
background: #f5f5f7;
}
.btn {
position: relative;
overflow: hidden;
padding: 12px 28px;
font-size: 16px;
font-weight: 600;
color: #0d6efd;
background: #fff;
border: 2px solid #0d6efd;
border-radius: 8px;
cursor: pointer;
}
.btn::before {
content: "";
position: absolute;
inset: 0;
background: #0d6efd;
transform: translateX(-100%);
transition: transform 0.25s ease-out;
z-index: 0;
}
.btn .btn-text {
position: relative;
z-index: 1;
}
.btn:hover .btn-text {
color: #fff;
}
.btn:hover::before {
transform: translateX(0);
}
@media (prefers-reduced-motion: reduce) {
.btn::before {
transition: none;
}
}
</style>
</head>
<body>
<button class="btn" type="button">
<span class="btn-text">Sweep fill on hover</span>
</button>
</body>
</html>Respecting motion preferences and checking real performance
- Animations use transform and/or opacity wherever possible, not width, top, or margin
- A transition (or @keyframes animation) has a sensible duration — usually 150-300ms for UI feedback, not something that feels sluggish
- A prefers-reduced-motion: reduce media query disables or shortens the animation for users who have asked their OS to minimize motion
- The effect has been checked at actual size and speed in a real browser, not just skimmed in code, since some easing curves look fine in theory and janky in practice
The visual effect matters less than the property driving it. The same scale-and-shadow hover can feel buttery smooth or noticeably janky depending only on whether it's implemented with transform/opacity or with layout-affecting properties — the CSS you choose is the performance budget.
Exercise: Buttons
When should a button element be used instead of an anchor styled to look like one?
Frequently Asked Questions
- How do I animate a button on hover in CSS?
- Put a
transitionon the button, then change the property in:hover. Transitiontransform,background-colororbox-shadow, and keep it around 150 to 250ms so the response feels immediate rather than sluggish. - Why does my button animation look janky?
- You are probably animating a property that forces layout, such as
width,height,topormargin. Animatetransformandopacityinstead. The browser can run those on the compositor without recalculating the page.