CSS Transitions and Transforms: Smooth Hover and Motion Effects
Animate state changes with transition and move, rotate, scale and skew elements with transform, using safe timing, performance and reduced-motion habits.
What you will learn
Without CSS transitions, every change on a page is instant: a button snaps from blue to red, a menu pops open, a card jumps. A short transition makes the same change feel smooth and polished. Transforms let you move, rotate, scale and skew elements without disturbing the layout around them. Used together they power most hover effects you see on modern sites. In this lesson you will learn how to write a transition, how to control its timing, how each transform works and how to combine them, and the performance and accessibility habits that keep your motion helpful instead of annoying.
The HTML we will style
<h1 class="page-title">Motion Lab</h1>
<!-- Part 1: transitions on buttons -->
<div class="row">
<button class="btn btn-color">Color fade</button>
<button class="btn btn-lift">Lift</button>
<button class="btn btn-grow">Grow</button>
</div>
<!-- Part 2: timing functions. Hover the track to start the race -->
<div class="track">
<div class="runner runner-linear">linear</div>
<div class="runner runner-ease">ease</div>
<div class="runner runner-ease-in-out">ease-in-out</div>
<div class="runner runner-steps">steps(5)</div>
</div>
<!-- Part 3: a card with a hover lift and an image zoom -->
<article class="card">
<div class="card-media">
<div class="card-image">Image</div>
</div>
<h3 class="card-title">Hover this card</h3>
<p class="card-text">The card lifts, and the picture zooms inside its frame.</p>
</article>
<!-- Part 4: static transforms -->
<div class="transform-demo">
<div class="shape shape-translate">translate</div>
<div class="shape shape-rotate">rotate</div>
<div class="shape shape-scale">scale</div>
<div class="shape shape-skew">skew</div>
<div class="shape shape-origin">origin</div>
<div class="shape shape-combo">combo</div>
</div>
| Selector | Element | Used for |
|---|---|---|
.row |
the <div> around the three buttons |
a flex row |
.btn |
all three <button>s |
shared button look |
.btn-color, .btn-lift, .btn-grow |
each button | a colour fade, a lift and a grow effect |
.track |
the <div> around four runners |
the container you hover to start the race |
.runner, .runner-linear, .runner-ease, .runner-ease-in-out, .runner-steps |
the four bars | each one uses a different timing function |
.card |
the <article> |
the element that lifts on hover |
.card-media |
the <div> wrapping the picture |
the frame that hides the overflow |
.card-image |
the <div> inside the frame |
the picture that zooms |
.card-title, .card-text |
the heading and the paragraph | text |
.transform-demo |
the wrapper <div> |
a flex row for the shapes |
.shape |
the six boxes | shared look of the boxes |
.shape-translate, .shape-rotate, .shape-scale, .shape-skew, .shape-origin, .shape-combo |
each box | one transform each |
Part 1: transitions
A transition tells the browser: “when this property changes, don’t jump, animate over a short time”. You need to say three things: which property, how long, and (optionally) how it speeds up and slows down.
/* the <button class="btn btn-color"> */
.btn-color {
background-color: royalblue;
transition: background-color 0.3s ease;
}
.btn-color:hover {
background-color: crimson;
}
When the mouse enters, the background changes from blue to red over 0.3 seconds instead of instantly. When the mouse leaves, it fades back.
The transition shorthand
transition: background-color 0.3s ease 0s;
/* property time duration timing-function delay */
| Part | Longhand | Meaning |
|---|---|---|
| property | transition-property |
which property to animate (all means every one that changes) |
| duration | transition-duration |
how long it takes: 0.3s or 300ms |
| timing function | transition-timing-function |
the speed curve: ease, linear, etc. |
| delay | transition-delay |
how long to wait before starting |
Only the duration is required, because without it the default is 0s (instant). Always write seconds or milliseconds with the unit.
Put the transition on the normal state
This is the number-one beginner mistake. Look at where the transition line is above: on .btn-color, not on .btn-color:hover. If it is only on :hover, the button fades in (on hover) but snaps back instantly when you leave, because the transition rule stops applying as soon as you leave. Put it on the base rule and it works in both directions.
What can be animated?
Transitions work for properties that have values “in between”: numbers, lengths, percentages, colours, shadows and transforms. They cannot animate properties with no middle ground, such as display: none to block, or font-family. Typical candidates:
background-color,color,border-coloropacity,box-shadowtransformwidth,height,padding,margin(they work, but see the performance warning below)
Transitioning several properties
Separate them with commas:
/* the <button class="btn btn-lift"> */
.btn-lift {
transition: transform 0.2s ease, box-shadow 0.2s ease;
}
.btn-lift:hover {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.25);
}
The button rises by 4px and gains a shadow, both smoothly, using the transform you will meet in a moment.
Be careful with
transition: all. It animates every property that changes, including ones you did not think about (like a width change from a media query or from font loading). It is fine for a quick test, but name the properties in real projects.
Timing functions
The timing function is the speed curve of the animation: does it start slowly, speed up, slow down at the end?
| Value | Feel |
|---|---|
linear |
constant speed, mechanical |
ease |
the default: starts quickly, slows towards the end |
ease-in |
starts slowly, ends fast (feels like falling) |
ease-out |
starts fast, ends slowly (feels natural for things entering) |
ease-in-out |
slow at both ends |
cubic-bezier(x1, y1, x2, y2) |
a custom curve (design tools can generate these) |
steps(n) |
jumps in n discrete steps instead of moving smoothly |
Run the race in the practice editor: hover over the track and all four runners travel the same distance in the same time, but with different curves.
/* the <div class="track">: hovering it starts the race */
.track {
padding: 12px;
background-color: #eeeeee;
margin-bottom: 32px;
}
/* all four <div class="runner"> bars */
.runner {
width: 120px;
margin-bottom: 8px;
padding: 8px 12px;
background-color: royalblue;
color: white;
}
.runner-linear { transition: transform 2s linear; }
.runner-ease { transition: transform 2s ease; }
.runner-ease-in-out { transition: transform 2s ease-in-out; }
.runner-steps { transition: transform 2s steps(5); }
/* when the TRACK is hovered, every runner moves */
.track:hover .runner {
transform: translateX(240px);
}
Notice that the hover is on the parent (.track:hover), and the rule changes the children (.runner). That is a handy pattern: hovering a whole card to move something inside it.
Good durations for interfaces: 150 to 300 milliseconds for small things like buttons and links, up to 500ms for larger movements. Shorter feels snappy, longer feels sluggish. A 2-second duration like our race is only for demonstration.
Delay
transition-delay waits before starting. A small delay on a hover menu can stop it from flashing open when the mouse merely passes over:
.btn-color {
transition: background-color 0.2s ease 0.1s; /* wait 0.1s, then fade over 0.2s */
}
Part 2: transforms
transform changes how an element is drawn: moved, turned, resized or slanted. The important property of transforms: they do not affect the layout. The element still occupies its original space, and neighbours do not move. That makes them cheap and safe for effects.
The main functions
/* the six <div class="shape"> boxes */
.transform-demo {
display: flex;
gap: 40px;
padding: 40px;
}
.shape {
width: 100px;
height: 100px;
display: grid;
place-items: center;
background-color: seagreen;
color: white;
font-size: 0.875rem;
}
.shape-translate { transform: translate(20px, 30px); } /* move right 20px and down 30px */
.shape-rotate { transform: rotate(15deg); } /* turn clockwise by 15 degrees */
.shape-scale { transform: scale(1.3); } /* 130% of the size */
.shape-skew { transform: skew(15deg, 0); } /* slant horizontally */
| Function | What it does | Notes |
|---|---|---|
translate(x, y) |
moves the element | also translateX() and translateY(). Percentages refer to the element’s own size, so translateX(100%) moves it exactly its own width |
rotate(angle) |
turns it around its centre | deg, turn (0.25turn) or rad. Negative values turn the other way |
scale(n) |
resizes it | 1 is normal, 1.5 is bigger, 0.5 is smaller. scale(x, y) stretches axes separately |
skew(x, y) |
slants it | rarely used on its own, but good for shapes |
transform-origin: the pivot point
Rotation and scaling happen around the element’s centre by default. transform-origin moves that pivot:
/* the <div class="shape shape-origin"> */
.shape-origin {
transform-origin: top left; /* pivot around the top-left corner */
transform: rotate(20deg);
}
Imagine a door: it rotates around its hinge, not around its middle. Values can be keywords (top left, bottom center), percentages or lengths.
Combining several transforms
List several functions in one transform, separated by spaces:
/* the <div class="shape shape-combo"> */
.shape-combo {
transform: translateY(-10px) rotate(10deg) scale(1.1);
}
Order matters. The functions are applied from right to left in terms of the coordinate system, so translate(100px) rotate(45deg) (move along the original axis, then turn) is not the same as rotate(45deg) translate(100px) (turn first, so the movement now follows the rotated direction). When a combination looks wrong, try reordering.
There is also a second way, using separate properties translate, rotate and scale, which are supported in all current browsers and are easier to animate independently:
.shape-combo {
translate: 0 -10px;
rotate: 10deg;
scale: 1.1;
}
Things to know about transforms
- Transforms do not apply to plain inline elements (such as a
<span>or<a>that is stilldisplay: inline). Give themdisplay: inline-block, or make them a flex or grid item. - An element with a transform creates its own stacking context, which can change how
z-indexworks around it. - A scaled element’s text can look blurry at fractional sizes. Check at the final size.
- 3D transforms exist too (
rotateX(),rotateY(),perspective), used for card flips and tilt effects. You can experiment with them later.
Putting transitions and transforms together
A card that lifts
/* the <article class="card"> */
.card {
width: 280px;
padding: 16px;
margin-bottom: 32px;
background-color: white;
border: 1px solid #e2e8f0;
border-radius: 12px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
transition: transform 0.25s ease, box-shadow 0.25s ease;
}
.card:hover {
transform: translateY(-6px);
box-shadow: 0 14px 28px rgba(0, 0, 0, 0.18);
}
An image that zooms inside a frame
The zoom effect needs two elements: a frame that clips, and the image that scales inside it.
/* the <div class="card-media">: the frame */
.card-media {
overflow: hidden; /* anything that grows beyond the frame is clipped */
border-radius: 8px;
}
/* the <div class="card-image">: the picture */
.card-image {
height: 140px;
display: grid;
place-items: center;
background-color: royalblue;
color: white;
transition: transform 0.4s ease;
}
/* hovering the WHOLE CARD zooms the picture */
.card:hover .card-image {
transform: scale(1.12);
}
Without overflow: hidden on the frame, the enlarged picture would spill out over the card.
A button that grows slightly
/* the <button class="btn btn-grow"> */
.btn-grow {
transition: transform 0.15s ease;
}
.btn-grow:hover,
.btn-grow:focus-visible {
transform: scale(1.08);
}
.btn-grow:active {
transform: scale(0.97); /* a tiny "press" feeling */
}
Always add :focus-visible next to :hover, so keyboard users get the same feedback.
Performance: animate the cheap properties
When the browser animates a property, it has to redraw the page. Some properties are cheap and some are expensive:
- Cheap:
transformandopacity. The browser can handle them on the graphics card without recalculating the layout. - Expensive:
width,height,top,left,margin,padding. They force the browser to re-calculate the position of other elements on every frame, which can cause jank, especially on phones.
So prefer transform: translateX(...) over animating left, and transform: scale() over animating width and height. This is why every effect in this lesson uses transform or colours and opacity.
Accessibility: respect reduced motion
Some people get dizzy or distracted by movement, and they set “reduce motion” in their operating system. CSS lets you honour that:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
transition-duration: 0.01ms !important;
animation-duration: 0.01ms !important;
}
}
This turns transitions into near-instant changes for those visitors. (This is one of the legitimate uses of !important.) Also remember:
- Never use motion as the only way to communicate something.
- Avoid large, fast movements and anything that flashes.
- Keep hover effects subtle.
Putting it together
*, *::before, *::after { box-sizing: border-box; }
body { font-family: Arial, Helvetica, sans-serif; }
/* Part 1: buttons */
.row { display: flex; gap: 12px; margin-bottom: 32px; }
.btn {
padding: 12px 20px;
border: none;
border-radius: 8px;
background-color: royalblue;
color: white;
font: inherit;
cursor: pointer;
}
.btn-color { transition: background-color 0.3s ease; }
.btn-color:hover, .btn-color:focus-visible { background-color: crimson; }
.btn-lift { transition: transform 0.2s ease, box-shadow 0.2s ease; }
.btn-lift:hover, .btn-lift:focus-visible {
transform: translateY(-4px);
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.25);
}
.btn-grow { transition: transform 0.15s ease; }
.btn-grow:hover, .btn-grow:focus-visible { transform: scale(1.08); }
.btn-grow:active { transform: scale(0.97); }
/* Part 2: timing functions */
.track { padding: 12px; margin-bottom: 32px; background-color: #eeeeee; }
.runner { width: 120px; margin-bottom: 8px; padding: 8px 12px; background-color: royalblue; color: white; }
.runner-linear { transition: transform 2s linear; }
.runner-ease { transition: transform 2s ease; }
.runner-ease-in-out { transition: transform 2s ease-in-out; }
.runner-steps { transition: transform 2s steps(5); }
.track:hover .runner { transform: translateX(240px); }
/* Part 3: card */
.card {
width: 280px;
padding: 16px;
margin-bottom: 32px;
border: 1px solid #e2e8f0;
border-radius: 12px;
box-shadow: 0 2px 6px rgba(0, 0, 0, 0.08);
transition: transform 0.25s ease, box-shadow 0.25s ease;
}
.card:hover { transform: translateY(-6px); box-shadow: 0 14px 28px rgba(0, 0, 0, 0.18); }
.card-media { overflow: hidden; border-radius: 8px; }
.card-image {
height: 140px;
display: grid;
place-items: center;
background-color: royalblue;
color: white;
transition: transform 0.4s ease;
}
.card:hover .card-image { transform: scale(1.12); }
/* Part 4: static transforms */
.transform-demo { display: flex; gap: 40px; padding: 40px; }
.shape {
width: 100px;
height: 100px;
display: grid;
place-items: center;
background-color: seagreen;
color: white;
font-size: 0.875rem;
}
.shape-translate { transform: translate(20px, 30px); }
.shape-rotate { transform: rotate(15deg); }
.shape-scale { transform: scale(1.3); }
.shape-skew { transform: skew(15deg, 0); }
.shape-origin { transform-origin: top left; transform: rotate(20deg); }
.shape-combo { transform: translateY(-10px) rotate(10deg) scale(1.1); }
/* Respect reduced motion */
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
transition-duration: 0.01ms !important;
}
}
Common mistakes
- Putting the
transitionon:hoveronly. The hover-in animates but the hover-out snaps. Put it on the base state. - Forgetting the duration. The default is
0s, so nothing animates. - Forgetting units:
transition: opacity 0.3is invalid. Write0.3s. - Trying to transition
display(or other non-numeric properties). Animateopacityandtransforminstead. - Using
transition: alleverywhere, which animates things you never meant to. - Animating
width,height,topandleft, which is slow. Usetransformandopacity. - Expecting transforms on inline elements like a plain
<span>or<a>. Make theminline-block. - Writing transforms in the wrong order, so the element moves in an unexpected direction.
- Writing two separate
transformdeclarations in one rule: the second replaces the first. Combine them into one. - Forgetting
overflow: hiddenon the frame of a zoom effect. - Long, bouncy, flashy effects. Subtle and short wins.
- Ignoring
prefers-reduced-motion.
Practice
- Click Practice in Editor. Finish the Part 1 buttons. First, give
.btn-lifta transition and a hover lift. Then move thetransitionline onto:hoverand see what changes when you move the mouse away. - Change the duration of
.btn-colorto1s, then0.1s. Which feels best for a button? - Run the race, then change the timing functions. Which runner arrives first? Which one starts fastest?
- Try
cubic-bezier(0.68, -0.55, 0.27, 1.55)as a timing function. What does the runner do? - Build the card hover effect with a lift and a picture zoom. Then remove
overflow: hiddenfrom.card-mediaand observe the problem. - Give all six
.shapeboxes their own transforms. Then change the origin of.shape-rotatetobottom right. - Compare
translate(100px) rotate(45deg)withrotate(45deg) translate(100px)on two boxes. Why do they end up in different places? - Add the
prefers-reduced-motionblock and test it by turning on “reduce motion” in your operating system or DevTools rendering options.
Recap
- A transition animates a property change:
transition: property duration timing-function delay;. Only the duration is required. - Put the transition on the normal state, not on
:hover, so it plays in both directions. - Common timing functions:
ease(default),linear,ease-in,ease-out,ease-in-out,cubic-bezier()andsteps(). Use 150 to 300ms for most UI effects. - Transforms (
translate,rotate,scale,skew) change how an element is drawn without affecting the layout.transform-originsets the pivot, and the order of combined transforms matters. - Transforms need a non-inline element. Pair a clipping frame (
overflow: hidden) with a scaling image for zoom effects. - Animate
transformandopacityfor smooth performance, and avoid animating layout properties. - Honour
prefers-reduced-motionand keep motion subtle. - Next you will go further with keyframe animations, which can loop, run on their own and play multi-step sequences.