Frontend

CSS Animations: Creating Smooth Transitions

Mayur Dabhi
Mayur Dabhi
March 11, 2026
20 min read

You add a hover effect to a card, it looks fine on your laptop, and then someone opens the page on a mid-range Android phone and the whole list stutters while scrolling. Or a dropdown "animates" by snapping open with no easing at all, because the transition was set on the wrong property. Most animation bugs I run into aren't about missing features. They come from animating the wrong thing, or animating it the wrong way.

This post covers CSS transitions and CSS keyframe animations: when to reach for each, how timing functions change the feel, which properties are cheap to animate, and how to respect users who've asked for less motion.

What Animation Is Actually For

Motion earns its place when it does a job:

  • Guide attention: point users at the thing that just changed
  • Provide feedback: confirm a click, a save, a state change
  • Create continuity: show where a panel came from so the change doesn't feel like a jump cut
  • Add a bit of character: fine in small doses, as long as it never slows anyone down

CSS Transitions vs. CSS Animations

These two get mixed up a lot. The short version: a transition reacts to a state change, an animation runs its own script.

Transitions vs. Animations CSS Transitions • Triggered by state changes (hover, focus) • Two states: start to end • Simpler syntax • Requires trigger to activate • Cannot loop automatically State A State B CSS Animations • Run automatically or on trigger • Multiple keyframe states • More control (delay, iterations) • Can run on page load • Supports infinite loops 0% 50% 75% 100%

Transitions handle simple A→B changes, while animations support complex multi-step sequences

Feature Transitions Animations
Complexity Simple (2 states) Complex (unlimited keyframes)
Trigger Requires state change Auto or triggered
Looping No Yes (infinite)
Use Case Hover effects, toggles Loading spinners, attention grabbers

CSS Transitions

When a property value changes (because of :hover, a class toggle, whatever), the browser normally jumps straight to the new value. A transition tells it to interpolate over some duration instead. That's the whole idea, and it covers most of the motion on a typical site.

The Transition Properties

transition-property

Which CSS property to animate

all | opacity | transform
transition-duration

How long the animation takes

0.3s | 300ms | 1s
transition-timing-function

The acceleration curve

ease | linear | ease-in-out
transition-delay

Wait before starting

0s | 0.2s | 500ms
CSS
.button {
  background-color: #a855f7;
  transform: scale(1);
  box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
  
  /* Shorthand: property duration timing-function delay */
  transition: all 0.3s ease;
  
  /* Or be specific (better for performance): */
  transition: 
    transform 0.3s ease,
    background-color 0.3s ease,
    box-shadow 0.3s ease;
}

.button:hover {
  background-color: #9333ea;
  transform: scale(1.05);
  box-shadow: 0 10px 20px rgba(168, 85, 247, 0.3);
}

Interactive Demo: Hover Transition

Hover over the box to see the transition in action

Performance Tip

I'd avoid transition: all in production. List the properties you actually mean to animate. With all, any property that changes later (a theme switch, a media query, a width set by JavaScript) suddenly animates too, which is both surprising and slower.

Gotcha: display and height: auto

Two things that trip people up constantly. First, display: none to display: block won't transition; the element just appears. Toggle opacity and visibility instead (or look into transition-behavior: allow-discrete in newer browsers). Second, you can't transition height to auto in most browsers today. The usual workarounds are animating max-height to a value that's comfortably bigger than the content, or using a grid trick with grid-template-rows: 0fr to 1fr.

Understanding Timing Functions

The timing function sets the acceleration curve: how the motion speeds up and slows down. Same duration, same distance, completely different feel. As a rule of thumb, ease-out works well for things entering the screen, ease-in for things leaving, and linear mostly for spinners and progress bars.

ease (default)cubic-bezier(0.25, 0.1, 0.25, 1)
linearcubic-bezier(0, 0, 1, 1)
ease-incubic-bezier(0.42, 0, 1, 1)
ease-outcubic-bezier(0, 0, 0.58, 1)
ease-in-outcubic-bezier(0.42, 0, 0.58, 1)
custom (bounce)cubic-bezier(0.68, -0.55, 0.265, 1.55)

Hover over each bar to see the different timing functions

Cubic Bezier Curve Visualization ease linear 0% 100% start end

Bezier curves define how progress maps to output over time

CSS Keyframe Animations

Once you need more than two states, want something to play on page load, or need it to loop, transitions run out of road. That's where @keyframes comes in.

Defining Keyframes

Keyframes define the waypoints your animation passes through. You can use percentages or the keywords from (0%) and to (100%).

CSS
/* Simple two-state animation */
@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

/* Multi-step animation with percentages */
@keyframes bounce {
  0%, 100% {
    transform: translateY(0);
    animation-timing-function: ease-out;
  }
  50% {
    transform: translateY(-30px);
    animation-timing-function: ease-in;
  }
}

/* Applying the animation */
.element {
  animation: bounce 1s infinite;
}

Animation Properties

CSS
.animated-element {
  /* Individual properties */
  animation-name: bounce;          /* which @keyframes to use */
  animation-duration: 1s;         /* how long one cycle takes */
  animation-timing-function: ease; /* acceleration curve */
  animation-delay: 0.5s;          /* wait before starting */
  animation-iteration-count: 3;   /* number | infinite */
  animation-direction: alternate; /* normal | reverse | alternate */
  animation-fill-mode: forwards;  /* none | forwards | backwards | both */
  animation-play-state: running; /* running | paused */
  
  /* Shorthand */
  animation: bounce 1s ease 0.5s 3 alternate forwards;
}

Live Animation Examples

Bounce

Pulse

Spin

Shake

View Keyframe Code for All Examples

/* Bounce Animation */
@keyframes bounce {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-30px); }
}

/* Pulse Animation */
@keyframes pulse {
  0%, 100% { 
    transform: scale(1); 
    opacity: 1; 
  }
  50% { 
    transform: scale(1.1); 
    opacity: 0.7; 
  }
}

/* Spin Animation */
@keyframes spin {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

/* Shake Animation */
@keyframes shake {
  0%, 100% { transform: translateX(0); }
  25% { transform: translateX(-10px); }
  75% { transform: translateX(10px); }
}

CSS Transform: Where Most Motion Should Live

If you only remember one thing from this post, make it this: animate transform instead of position or size. Browsers can usually handle transforms on the compositor (often on the GPU) without recalculating layout, so the main thread stays free and the motion stays smooth, especially on phones.

Transform Functions translate() Move position scale() Resize element rotate() Rotate degrees skew() Slant element Combining Transforms Original → scale + rotate → + translate

Transform functions can be chained in one declaration (order matters)

CSS
/* Individual transforms */
transform: translateX(100px);      /* Move horizontally */
transform: translateY(-50px);      /* Move vertically */
transform: translate(100px, 50px); /* Move both axes */
transform: scale(1.5);             /* Scale uniformly */
transform: scale(1.5, 0.8);        /* Scale X and Y separately */
transform: rotate(45deg);          /* Rotate clockwise */
transform: skew(10deg, 5deg);      /* Skew X and Y */

/* Combine multiple transforms (order matters!) */
transform: translateY(-10px) scale(1.1) rotate(5deg);

/* 3D transforms */
transform: perspective(1000px) rotateY(45deg);
transform: translateZ(100px);       /* Move toward viewer */

/* Transform origin (pivot point) */
transform-origin: center center;  /* Default */
transform-origin: top left;       /* Rotate from corner */
transform-origin: 50% 100%;       /* Bottom center */
The Cheap Properties

These two are the ones browsers can reliably animate on the compositor at 60fps:

  • transform: translate, scale, rotate, skew
  • opacity: fades

Animating width, height, top, left, margin, or padding forces layout on every frame. On a small element you may get away with it; on a big list it's where the jank comes from.

Real-World Animation Patterns

These four show up in almost every project I touch. Copy them, then tune durations to taste.

1. Fade In on Scroll

CSS
.fade-in-up {
  opacity: 0;
  transform: translateY(30px);
  transition: opacity 0.6s ease, transform 0.6s ease;
}

.fade-in-up.visible {
  opacity: 1;
  transform: translateY(0);
}

/* Staggered children */
.fade-in-up:nth-child(1) { transition-delay: 0.1s; }
.fade-in-up:nth-child(2) { transition-delay: 0.2s; }
.fade-in-up:nth-child(3) { transition-delay: 0.3s; }

2. Loading Spinner

CSS
.spinner {
  width: 40px;
  height: 40px;
  border: 3px solid rgba(255, 255, 255, 0.1);
  border-top-color: #a855f7;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}

@keyframes spin {
  to { transform: rotate(360deg); }
}

3. Button Hover Effect

CSS
.btn {
  position: relative;
  overflow: hidden;
  transition: transform 0.3s ease, box-shadow 0.3s ease;
}

.btn::before {
  content: '';
  position: absolute;
  top: 0;
  left: -100%;
  width: 100%;
  height: 100%;
  background: linear-gradient(90deg, transparent, rgba(255,255,255,0.2), transparent);
  transition: left 0.5s ease;
}

.btn:hover {
  transform: translateY(-2px);
  box-shadow: 0 10px 20px rgba(168, 85, 247, 0.3);
}

.btn:hover::before {
  left: 100%;
}

4. Skeleton Loading Screen

CSS
.skeleton {
  background: linear-gradient(
    90deg,
    #1a1a1a 25%,
    #2a2a2a 50%,
    #1a1a1a 75%
  );
  background-size: 200% 100%;
  animation: shimmer 1.5s infinite;
  border-radius: 8px;
}

@keyframes shimmer {
  0% { background-position: 200% 0; }
  100% { background-position: -200% 0; }
}

Accessibility Considerations

For people with vestibular disorders, big sliding and zooming effects can cause real dizziness or nausea. Operating systems let users turn on "reduce motion," and CSS can read that setting. Honor it:

CSS
/* Respect user's motion preferences */
@media (prefers-reduced-motion: reduce) {
  *,
  ::before,
  ::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
  }
}

/* Or provide alternative, subtle animations */
@media (prefers-reduced-motion: reduce) {
  .fancy-animation {
    animation: none;
    opacity: 1; /* Show immediately instead of fade */
  }
}
Animation Safety Rules
  • No flashing: never flash content more than 3 times per second (seizure risk, and a WCAG requirement)
  • Provide controls: anything that loops for more than 5 seconds should be pausable
  • Keep it subtle: large movements are the ones that trigger vertigo
  • Test with reduced motion on: make sure nothing stays hidden at opacity: 0 when the animation is skipped

Debugging & Performance

1

Use DevTools Animation Panel

Chrome DevTools has an Animations panel (More Tools → Animations). It records animations as they run and lets you replay them at 25% or 10% speed, which is the easiest way to spot a bad easing curve.

2

Check Paint Flashing

Turn on "Paint flashing" in the DevTools Rendering tab. Areas that repaint flash green. If a transform animation lights up the whole card on every frame, something else on it (a shadow, a filter) is being repainted too.

3

Monitor Frame Rate

Record in the Performance tab with CPU throttling set to 4x or 6x, since your dev machine hides problems. At 60fps you have about 16.7ms per frame. Long purple "Layout" blocks during an animation usually mean you're animating a layout property.

4

Use will-change Sparingly

will-change tells the browser to prepare a separate layer ahead of time. Each layer costs GPU memory, so putting it on dozens of elements can make things worse. Apply it only to elements that are about to animate.

CSS
/* Use will-change to optimize (sparingly!) */
.animated-element {
  will-change: transform, opacity;
}

/* Remove after animation completes */
.animated-element.done {
  will-change: auto;
}

When I'd Use CSS Animation (and When I Wouldn't)

For hover states, focus rings, dropdowns, toasts, spinners and skeleton loaders, plain CSS is what I reach for first. It's declarative, it doesn't block the main thread when you stick to transform and opacity, and prefers-reduced-motion handles accessibility in a few lines.

I wouldn't force it in a few cases:

Keep durations short (most UI transitions feel right somewhere around 150 to 300ms) and test on a slow phone before you ship.

CSS Animations Transitions Transform UI/UX Frontend Performance
Mayur Dabhi

Mayur Dabhi

Full-stack developer passionate about creating beautiful, performant web experiences. I love turning complex problems into elegant solutions.