Transitions & Animations
Motion makes an interface feel responsive. A button that gently lifts on hover, a menu that fades in, a loading spinner, or a notification that slides into view all help users understand what is happening.
CSS has two main motion tools:
- Transitions: smooth changes between two states.
- Animations: named motion sequences with one or more keyframes.
Use motion to guide attention, explain state changes, and make interactions feel polished. Do not use motion just because it is possible.
1. Motion Mental Model
Most CSS motion is a change from one visual state to another.
.button {
background: #2563eb;
}
.button:hover {
background: #1e40af;
}
Without a transition, the background changes instantly. With a transition, the browser gradually interpolates from the old value to the new value.
.button {
background: #2563eb;
transition: background-color 200ms ease;
}
.button:hover {
background: #1e40af;
}
2. Transitions: Smooth Interaction
A transition says: "When this property changes, animate the change over a duration."
.card {
transform: translateY(0);
transition: transform 180ms ease;
}
.card:hover {
transform: translateY(-4px);
}
The transition belongs on the base state, not only on :hover. That way the element animates both when the hover starts and when it ends.
Bad:
.card:hover {
transform: translateY(-4px);
transition: transform 180ms ease;
}
Better:
.card {
transition: transform 180ms ease;
}
.card:hover {
transform: translateY(-4px);
}
3. Core Transition Properties
Transitions are made from four properties.
.button {
transition-property: background-color;
transition-duration: 200ms;
transition-timing-function: ease;
transition-delay: 0ms;
}
| Property | Purpose | Example |
|---|---|---|
transition-property | What property should animate | transform |
transition-duration | How long it takes | 200ms |
transition-timing-function | How speed changes over time | ease-out |
transition-delay | How long to wait before starting | 100ms |
Most projects use shorthand:
.button {
transition: transform 180ms ease, background-color 180ms ease;
}
4. Transition Shorthand Syntax
The shorthand pattern is:
transition: property duration timing-function delay;
Examples:
.quick {
transition: opacity 120ms linear;
}
.smooth {
transition: transform 240ms ease-out;
}
.delayed {
transition: color 180ms ease 100ms;
}
You can animate multiple properties:
.button {
transition:
background-color 180ms ease,
transform 180ms ease,
box-shadow 180ms ease;
}
Avoid transition: all in production code unless you intentionally want every animatable property to transition. It can create unexpected motion when unrelated properties change.
5. Timing Functions: The Feel of Motion
The timing function controls acceleration.
.item {
transition-timing-function: ease-out;
}
Common values:
linear: constant speed.ease: slow start, faster middle, slow end.ease-in: starts slow, ends fast.ease-out: starts fast, ends slow.ease-in-out: slow start and slow end.cubic-bezier(...): custom curve.steps(...): jumps in discrete steps.
Simple guidance:
- Use
ease-outfor elements entering or responding to user input. - Use
ease-infor elements leaving. - Use
ease-in-outfor movement between two stable positions. - Use
linearfor infinite rotation, like spinners.
6. Duration: How Long Should Motion Take?
Good UI motion is usually short.
| Motion type | Typical duration |
|---|---|
| Hover feedback | 120ms to 200ms |
| Small menu or tooltip | 150ms to 250ms |
| Modal entrance | 200ms to 350ms |
| Page section entrance | 300ms to 600ms |
| Decorative loop | Depends, often 1s to 3s |
Too fast feels abrupt. Too slow feels heavy.
.fast-feedback {
transition: transform 140ms ease-out;
}
.modal {
transition: opacity 240ms ease, transform 240ms ease;
}
7. Transforms: The Engine of Smooth Motion
The transform property changes how an element is drawn without changing normal document flow.
Common transform functions:
.move {
transform: translateX(24px);
}
.lift {
transform: translateY(-4px);
}
.grow {
transform: scale(1.05);
}
.turn {
transform: rotate(8deg);
}
.tilt {
transform: skewX(-8deg);
}
Transforms are usually more performant than animating layout properties like top, left, width, or height.
8. Transform Origin
transform-origin controls the pivot point for transforms.
.door {
transform-origin: left center;
}
.door:hover {
transform: rotateY(18deg);
}
Common values:
centertop leftleft centerbottom center50% 50%20px 80px
For example, a scale animation can feel very different depending on whether it grows from the center or from the top-left corner.
.menu {
transform-origin: top right;
}
This is useful for dropdown menus that open from a trigger button.
9. Practical Transition: Button Lift
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 0.75rem 1rem;
border: 0;
border-radius: 12px;
color: white;
background: #2563eb;
box-shadow: 0 8px 18px rgb(37 99 235 / 20%);
transition:
transform 160ms ease-out,
box-shadow 160ms ease-out,
background-color 160ms ease-out;
}
.btn:hover {
background: #1d4ed8;
transform: translateY(-2px);
box-shadow: 0 12px 24px rgb(37 99 235 / 28%);
}
.btn:active {
transform: translateY(0);
box-shadow: 0 6px 14px rgb(37 99 235 / 18%);
}
This uses three states:
- default,
- hover,
- active.
The hover state lifts the button. The active state presses it back down.
10. Practical Transition: Focus Ring
Motion is not only for hover. Keyboard users need clear focus states.
.input {
border: 1px solid #cbd5e1;
outline: 3px solid transparent;
outline-offset: 2px;
transition: border-color 160ms ease, outline-color 160ms ease;
}
.input:focus-visible {
border-color: #2563eb;
outline-color: #93c5fd;
}
Use :focus-visible when you want focus styles mostly for keyboard navigation.
Do not remove outlines without replacement:
/* Bad */
button:focus {
outline: none;
}
11. Practical Transition: Dropdown Menu
.menu {
opacity: 0;
transform: translateY(-8px) scale(0.98);
transform-origin: top right;
pointer-events: none;
transition: opacity 160ms ease, transform 160ms ease;
}
.menu.is-open {
opacity: 1;
transform: translateY(0) scale(1);
pointer-events: auto;
}
This pattern combines:
opacityfor fade,transformfor movement,pointer-eventsso hidden menus cannot be clicked,- and a state class for control.
For JavaScript-driven UI, toggling .is-open is often cleaner than relying only on :hover.
12. Animations: Motion That Has a Timeline
Transitions need a property change. Animations can run automatically because they define their own timeline with @keyframes.
@keyframes slide-in {
from {
opacity: 0;
transform: translateX(-24px);
}
to {
opacity: 1;
transform: translateX(0);
}
}
.notification {
animation: slide-in 300ms ease-out forwards;
}
Use animations when:
- motion should start automatically,
- motion has multiple steps,
- motion repeats,
- or motion does not depend directly on a user state like hover.
13. Keyframes
Keyframes define values at points in the animation.
@keyframes fade-slide {
0% {
opacity: 0;
transform: translateY(12px);
}
60% {
opacity: 1;
}
100% {
opacity: 1;
transform: translateY(0);
}
}
from and to are aliases for 0% and 100%.
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
14. Animation Properties
Animations have several properties.
.box {
animation-name: fade-slide;
animation-duration: 400ms;
animation-timing-function: ease-out;
animation-delay: 100ms;
animation-iteration-count: 1;
animation-direction: normal;
animation-fill-mode: forwards;
}
| Property | Meaning |
|---|---|
animation-name | Which @keyframes to use |
animation-duration | How long one cycle takes |
animation-timing-function | Speed curve |
animation-delay | Wait before starting |
animation-iteration-count | Number of times to run |
animation-direction | Normal, reverse, alternate |
animation-fill-mode | What styles apply before/after |
animation-play-state | Running or paused |
Shorthand:
.box {
animation: fade-slide 400ms ease-out 100ms 1 normal forwards;
}
15. animation-fill-mode
animation-fill-mode is important because it controls what happens before and after the animation.
.toast {
animation: slide-in 300ms ease-out forwards;
}
Common values:
none: do not apply keyframe styles outside active animation time.forwards: keep the final keyframe after animation ends.backwards: apply the first keyframe during the delay.both: apply bothforwardsandbackwards.
Without forwards, an entrance animation may jump back to its starting state after finishing.
16. Practical Animation: Loading Spinner
Spinners use a repeating linear rotation.
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.loader {
width: 40px;
height: 40px;
border: 4px solid #e5e7eb;
border-top-color: #2563eb;
border-radius: 50%;
animation: spin 800ms linear infinite;
}
Why linear? A spinner should rotate at a constant speed.
17. Practical Animation: Pulse
A pulse draws attention to a small element such as a notification dot.
@keyframes pulse {
0% {
transform: scale(1);
box-shadow: 0 0 0 0 rgb(37 99 235 / 40%);
}
70% {
transform: scale(1.05);
box-shadow: 0 0 0 12px rgb(37 99 235 / 0%);
}
100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgb(37 99 235 / 0%);
}
}
.notification-dot {
animation: pulse 2s ease-out infinite;
}
Use pulse effects sparingly. They intentionally compete for attention.
18. Practical Animation: Staggered Cards
Staggering means several elements animate with slightly different delays.
@keyframes card-enter {
from {
opacity: 0;
transform: translateY(16px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
opacity: 0;
animation: card-enter 420ms ease-out forwards;
}
.card:nth-child(1) {
animation-delay: 80ms;
}
.card:nth-child(2) {
animation-delay: 160ms;
}
.card:nth-child(3) {
animation-delay: 240ms;
}
Staggering helps users perceive groups of related content.
19. Practical Animation: Skeleton Loading
Skeleton screens show that content is loading.
@keyframes shimmer {
from {
background-position: 200% 0;
}
to {
background-position: -200% 0;
}
}
.skeleton {
border-radius: 12px;
background:
linear-gradient(90deg, #e5e7eb 25%, #f8fafc 37%, #e5e7eb 63%);
background-size: 400% 100%;
animation: shimmer 1.4s ease infinite;
}
Use skeletons when the structure of the loading content is predictable. For unknown content, a simple loading message may be clearer.
20. Practical Animation: Modal Entrance
A modal often combines a fading overlay with a scaling panel.
.overlay {
opacity: 0;
transition: opacity 180ms ease;
}
.modal {
opacity: 0;
transform: translateY(12px) scale(0.98);
transition: opacity 220ms ease, transform 220ms ease;
}
.dialog.is-open .overlay {
opacity: 1;
}
.dialog.is-open .modal {
opacity: 1;
transform: translateY(0) scale(1);
}
This makes the modal feel like it enters the page instead of appearing abruptly.
Accessibility note: visual animation is not enough. A real modal also needs focus management, keyboard escape behavior, and proper ARIA semantics.
21. Animatable Properties
Not every property animates well.
Good choices:
transformopacityfilterin small amountsbackground-colorcolorbox-shadowin moderation
Avoid animating often:
widthheighttopleftrightbottommarginpadding
These layout-affecting properties can cause reflow and make animation less smooth.
Instead of:
.panel:hover {
left: 20px;
}
Use:
.panel:hover {
transform: translateX(20px);
}
22. Rendering Cost: Layout, Paint, Composite
When CSS changes, the browser may do different amounts of work.
| Work | Meaning | Example trigger |
|---|---|---|
| Layout | Recalculate sizes and positions | width, height, top |
| Paint | Redraw pixels | background, box-shadow |
| Composite | Move already-painted layers | transform, opacity |
For smooth UI motion, prefer composite-friendly properties.
23. will-change
will-change tells the browser an element is likely to change.
.card {
will-change: transform;
}
Use it carefully. It can improve some animations, but overusing it wastes memory.
Good use:
.drag-preview {
will-change: transform;
}
Bad use:
* {
will-change: transform;
}
Only use will-change for elements that truly need it.
24. Pausing and Controlling Animations
animation-play-state can pause an animation.
.marquee {
animation: scroll 12s linear infinite;
}
.marquee:hover {
animation-play-state: paused;
}
This is useful for motion that users may want to inspect or stop.
You can also start and stop animations by adding or removing a class:
.panel {
animation: none;
}
.panel.is-entering {
animation: panel-enter 240ms ease-out forwards;
}
25. Respecting Reduced Motion
Some users prefer reduced motion because animation can cause dizziness, distraction, or discomfort.
Use the prefers-reduced-motion media query:
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
scroll-behavior: auto !important;
transition-duration: 0.01ms !important;
}
}
For a more targeted approach:
@media (prefers-reduced-motion: reduce) {
.card {
animation: none;
transition: none;
}
}
Do not remove important state changes. Remove or simplify the motion used to communicate them.
26. Motion Design Rules
Use these rules to keep motion professional:
- Motion should explain, not decorate.
- Keep common interactions fast.
- Use consistent timing across similar components.
- Animate transform and opacity when possible.
- Avoid large page-wide motion unless it has a purpose.
- Do not animate every element at once.
- Respect reduced-motion preferences.
- Make final states stable and readable.
27. Complete Example: Interactive Course Card
.course-card {
position: relative;
overflow: hidden;
border: 1px solid #d6d3d1;
border-radius: 24px;
background: #fffdf8;
box-shadow: 0 14px 30px rgb(15 23 42 / 8%);
transition:
transform 180ms ease-out,
box-shadow 180ms ease-out,
border-color 180ms ease-out;
}
.course-card::before {
content: "";
position: absolute;
inset: 0;
background: radial-gradient(circle at top left, rgb(251 191 36 / 18%), transparent 40%);
opacity: 0;
transition: opacity 180ms ease-out;
}
.course-card:hover {
transform: translateY(-4px);
border-color: #c9c1af;
box-shadow: 0 22px 44px rgb(15 23 42 / 12%);
}
.course-card:hover::before {
opacity: 1;
}
.course-card:focus-within {
outline: 3px solid #93c5fd;
outline-offset: 4px;
}
@media (prefers-reduced-motion: reduce) {
.course-card,
.course-card::before {
transition: none;
}
.course-card:hover {
transform: none;
}
}
This example combines:
- hover lift,
- shadow change,
- subtle background reveal,
- focus visibility,
- and reduced-motion support.
28. Debugging Checklist
When motion does not work, check:
- Is the property animatable?
- Is the transition on the base state?
- Does the state actually change the property?
- Is the duration too short to notice?
- Is another rule overriding the transition or animation?
- Does the animation name match the
@keyframesname exactly? - Is
animation-fill-modeneeded? - Is
display: nonepreventing a transition? - Is
prefers-reduced-motiondisabling motion? - Are you animating expensive layout properties?
29. Mini Practice
Create a card hover effect:
- lift the card by
4px, - increase its shadow,
- animate both changes,
- and remove the transform for reduced-motion users.
Possible answer:
.practice-card {
box-shadow: 0 8px 18px rgb(15 23 42 / 8%);
transition: transform 180ms ease-out, box-shadow 180ms ease-out;
}
.practice-card:hover {
transform: translateY(-4px);
box-shadow: 0 18px 36px rgb(15 23 42 / 12%);
}
@media (prefers-reduced-motion: reduce) {
.practice-card {
transition: box-shadow 180ms ease;
}
.practice-card:hover {
transform: none;
}
}
Create a fade-in animation:
@keyframes fade-in {
from {
opacity: 0;
}
to {
opacity: 1;
}
}
.fade-in {
animation: fade-in 240ms ease-out forwards;
}
Create a slide-and-fade animation:
@keyframes slide-and-fade {
from {
opacity: 0;
transform: translateY(12px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.slide-and-fade {
animation: slide-and-fade 320ms ease-out both;
}