Skip to main content

CSS properties for creating animations and smooth transitions

CSS transitions and animations are two separate tools for adding motion to the web, and they solve different problems.

Theory

TL;DR

  • transition reacts to a state change (hover, focus, class toggle) and waits for a trigger
  • animation with @keyframes plays on its own - no trigger needed, can loop, can reverse
  • Main difference: transition is a single A to B change; animation is a sequence with as many steps as you define
  • Best for performance: animate transform and opacity - they run on the GPU and skip layout recalculation
  • Animating width, height, top, or left causes reflow every frame - use transform equivalents instead

Quick example

css
/* transition: reacts to :hover */ .button { background: #3b82f6; transition: background 0.3s ease, transform 0.15s ease; } .button:hover { background: #1d4ed8; transform: scale(1.04); } /* animation: plays on its own, no trigger needed */ @keyframes fadeIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; transform: translateY(0); } } .modal { animation: fadeIn 0.25s ease-out forwards; }

transition waits. animation just goes.

transition: triggered changes

transition takes four values: which property, how long, what easing, and an optional delay. Multiple properties separate with commas:

css
.card { transition: box-shadow 0.2s ease, transform 0.2s ease; } .card:hover { box-shadow: 0 8px 24px rgba(0,0,0,0.15); transform: translateY(-4px); }

transition: all 0.3s technically works. But when you add a property later and it starts animating unexpectedly, you'll spend time figuring out why. List the properties explicitly.

Easing options: ease starts fast and slows down (default), linear keeps constant speed, ease-out decelerates toward the end. For UI interactions, ease-out feels the most natural because it mirrors how physical objects stop. cubic-bezier() gives you exact control over the curve.

animation + @keyframes: scripted motion

@keyframes describes motion as a sequence of snapshots. from/to is identical to 0%/100%, but you can add percentage stops at any point:

css
@keyframes pulse { 0% { transform: scale(1); } 50% { transform: scale(1.08); } 100% { transform: scale(1); } } .badge { animation: pulse 1.5s ease-in-out infinite; }

The key animation sub-properties:

  • animation-name - which @keyframes block to use
  • animation-duration - how long one cycle takes
  • animation-timing-function - same easing options as transition
  • animation-delay - wait before the first play
  • animation-iteration-count - 1, 3, or infinite
  • animation-direction - normal, reverse, alternate (plays forward then backward)
  • animation-fill-mode - what state the element holds before or after playing

animation-fill-mode: forwards is worth memorizing on its own. Without it, an element snaps back to its original state the moment the animation ends. That breaks most fade-in implementations.

What to animate for performance

Browsers paint the page in layers. transform and opacity changes happen on the compositor thread, which runs separately and never touches layout. Every other property (width, height, padding, left, top) forces the browser to recalculate positions or repaint.

css
/* triggers reflow every frame - slow */ .bad { transition: width 0.3s ease; } /* stays on compositor thread - fast */ .good { transition: transform 0.3s ease; }

To animate an element growing, use transform: scaleX() or scaleY() instead of width or height. For sliding elements in or out, transform: translateX() beats left every time.

will-change: transform hints to the browser to promote an element to its own compositor layer before the animation starts. From experience in production: adding it blindly to everything is a common over-optimization. Profile first, then add it only where you actually see jank - each promoted layer takes GPU memory.

Common mistakes

Animating layout properties. Changing height or width in a loop drops frames. Use transform: scaleY() for expanding elements instead.

transition: all. Works, but causes unexpected animations on unrelated properties. List properties explicitly.

Missing animation-fill-mode: forwards. An element fades in beautifully, then snaps back to opacity: 0. Add forwards and the final keyframe state persists.

Using @keyframes for hover effects. A hover change is exactly two states. That is what transition exists for. @keyframes earns its place when you need three or more states, or when the animation runs without any user interaction.

No prefers-reduced-motion support. Some users have vestibular conditions where motion causes real discomfort. Small effort, real impact:

css
@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; transition-duration: 0.01ms !important; } }

Follow-up questions

Q: Which CSS properties are safe to animate for performance?
A: transform and opacity. They run on the compositor thread without triggering layout or repaint. Properties like width, height, top, and left cause reflow on every frame.

Q: What does animation-fill-mode: forwards do?
A: It tells the element to keep the styles from the last keyframe after the animation finishes. Without it, the element reverts to its original styles the moment the animation ends.

Q: Can you run multiple animations on one element?
A: Yes, with a comma-separated list: animation: fadeIn 0.3s ease, pulse 1s ease-in-out 0.3s infinite. Each animation gets its own timing and they overlap independently.

Q: When does transition not work?
A: On properties with no calculable midpoint. You cannot transition display: none to display: block because there is no middle state. For show/hide effects, transition opacity combined with visibility, or use transform: scale(0) to scale down to nothing.

Q: What is the difference between animation-direction: alternate and writing two separate keyframe sequences?
A: alternate reverses automatically on odd-numbered iterations using the same easing. Two separate sequences give you independent control over forward and reverse curves - useful when you want slow ease-in going out and a fast snap coming back.

Examples

Button hover with transition

css
.btn { background: #3b82f6; color: white; padding: 10px 20px; border: none; border-radius: 6px; cursor: pointer; /* two properties, each with its own timing */ transition: background 0.25s ease, transform 0.15s ease; } .btn:hover { background: #2563eb; transform: scale(1.03); } .btn:active { transform: scale(0.97); /* pressed feedback */ }

background takes 0.25s while transform responds in 0.15s. That slight difference feels more natural than locking both to the same speed - the scale snaps crisply while the color follows.

Loading spinner with animation

css
@keyframes spin { from { transform: rotate(0deg); } to { transform: rotate(360deg); } } .spinner { width: 24px; height: 24px; border: 3px solid #e5e7eb; border-top-color: #3b82f6; border-radius: 50%; animation: spin 0.75s linear infinite; }

linear is the right call here. A spinner with ease speeds up and slows down on every loop, which looks like it is struggling. Constant speed is what you want.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?