On this page

CSS transitions and animations

12 min read TextCh. 4 — Visual Styles

Transitions vs animations

CSS offers two mechanisms for adding motion:

  • Transitions: animate the change between two states (for example, hover)
  • Animations with @keyframes: define complex sequences with multiple steps

Transitions

A transition smooths the change of a CSS property from one value to another. It is defined on the base state (not on :hover).

The transition property

.element {
  transition: property duration timing-function delay;
}
Parameter Description Example
property Which property to animate opacity, transform, all
duration How long it lasts 200ms, 0.3s
timing-function Speed curve ease, linear, ease-in-out
delay Wait before starting 0ms, 100ms

Practical example

.button {
  background: #1a1a2e;
  transition: background 200ms ease, transform 150ms ease;
}

.button:hover {
  background: #3a3a5e;
  transform: translateY(-2px);
}

Multiple transitions

Separate each property with a comma. Avoid transition: all because it can animate unexpected properties and affect performance.

Timing functions

Function Behavior
ease Slow start, fast middle, slow end (default)
ease-in Slow start, fast end
ease-out Fast start, slow end
ease-in-out Slow on both ends
linear Constant speed
cubic-bezier() Custom curve

For user interactions, ease-out feels more natural because it responds quickly to input.

Interaction type Duration
Hover, focus 150-200ms
Menu opening 200-300ms
Page transition 300-500ms
Entry animation 400-600ms

More than 500ms feels slow. Less than 100ms is imperceptible.

Animations with @keyframes

For more complex movements, @keyframes lets you define multiple steps:

@keyframes slide-in {
  0% {
    opacity: 0;
    transform: translateX(-20px);
  }
  100% {
    opacity: 1;
    transform: translateX(0);
  }
}

.panel {
  animation: slide-in 400ms ease both;
}

Animation properties

Property Description
animation-name Name of the @keyframes
animation-duration Total duration
animation-timing-function Speed curve
animation-delay Wait before starting
animation-iteration-count Number of repetitions (1, infinite)
animation-direction Direction (normal, reverse, alternate)
animation-fill-mode Final state (forwards, backwards, both)

Shorthand

.element {
  animation: name 400ms ease 0ms 1 normal both;
  /*         name dur   timing delay count dir fill */
}

Staggered animations

To animate a list of elements with a cascading effect, use incremented animation-delay:

.item:nth-child(1) { animation-delay: 0ms; }
.item:nth-child(2) { animation-delay: 75ms; }
.item:nth-child(3) { animation-delay: 150ms; }

Or with custom properties for greater flexibility:

.item {
  animation: appear 400ms ease both;
  animation-delay: calc(var(--i) * 75ms);
}
<li class="item" style="--i: 0">First</li>
<li class="item" style="--i: 1">Second</li>
<li class="item" style="--i: 2">Third</li>

Animation performance

The browser can efficiently animate only two properties without causing repaint:

  • transform (translate, scale, rotate)
  • opacity

Animating width, height, margin, top, left forces the browser to recalculate the layout of the entire page (reflow), which is expensive.

Accessibility: prefers-reduced-motion

Some users experience dizziness or discomfort with animations. Respect their preference:

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

Animations bring your interface to life. In the next lesson, we will learn custom properties, CSS's native variables that make your code maintainable and dynamic.

Practice

  1. Add transitions to a button: Create a button with transitions on background and transform on hover. Use ease-out and a duration of 200ms for a natural feel.
  2. Create a loading spinner: Implement a circular spinner using @keyframes with a 360-degree rotation and animation: spin 800ms linear infinite.
  3. Respect prefers-reduced-motion: Add a @media (prefers-reduced-motion: reduce) block that reduces the duration of all animations and transitions to 0.01ms.
Performance
Only animate properties that the browser can optimize with the GPU: transform (translate, scale, rotate) and opacity. Avoid animating width, height, margin, or padding because they cause reflow and are more expensive.
Motion accessibility
Always include @media (prefers-reduced-motion: reduce) to respect users who have motion sensitivity. Reduce or eliminate animations in that case.
/* Basic button transition */
.button {
  background: #1a1a2e;
  color: white;
  padding: 0.75rem 1.5rem;
  border: none;
  border-radius: 8px;
  cursor: pointer;
  transition: background 200ms ease,
              transform 150ms ease;
}

.button:hover {
  background: #2d2d44;
  transform: translateY(-2px);
}

.button:active {
  transform: translateY(0) scale(0.98);
  transition-duration: 50ms;
}

/* Card with multiple transitions */
.card {
  background: white;
  border-radius: 12px;
  padding: 1.5rem;
  border: 1px solid #e0e0e0;
  transition: border-color 200ms ease,
              box-shadow 300ms ease;
}

.card:hover {
  border-color: #b056ff;
  box-shadow: 0 8px 24px rgb(176 86 255 / 15%);
}

/* Staggered entry transition with delay */
.list-item {
  opacity: 0;
  transform: translateY(10px);
  animation: appear 400ms ease forwards;
}

.list-item:nth-child(1) { animation-delay: 0ms; }
.list-item:nth-child(2) { animation-delay: 75ms; }
.list-item:nth-child(3) { animation-delay: 150ms; }
.list-item:nth-child(4) { animation-delay: 225ms; }
/* Fade-in animation */
@keyframes appear {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.section {
  animation: appear 600ms ease both;
}

/* Loading spinner */
@keyframes spin {
  to { rotate: 1turn; }
}

.spinner {
  width: 40px;
  height: 40px;
  border: 3px solid #e0e0e0;
  border-top-color: #b056ff;
  border-radius: 50%;
  animation: spin 800ms linear infinite;
}

/* Notification pulse */
@keyframes pulse {
  0%, 100% { scale: 1; }
  50% { scale: 1.15; }
}

.badge-notification {
  background: #e6286a;
  color: white;
  border-radius: 50%;
  width: 20px;
  height: 20px;
  animation: pulse 2s ease-in-out infinite;
}

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