Transitions & Animations

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;
}
instant jumpsmooth transition over time

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;
}
PropertyPurposeExample
transition-propertyWhat property should animatetransform
transition-durationHow long it takes200ms
transition-timing-functionHow speed changes over timeease-out
transition-delayHow long to wait before starting100ms

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.
progresstimelinearease-outease-in-out

Simple guidance:

  • Use ease-out for elements entering or responding to user input.
  • Use ease-in for elements leaving.
  • Use ease-in-out for movement between two stable positions.
  • Use linear for infinite rotation, like spinners.

6. Duration: How Long Should Motion Take?

Good UI motion is usually short.

Motion typeTypical duration
Hover feedback120ms to 200ms
Small menu or tooltip150ms to 250ms
Modal entrance200ms to 350ms
Page section entrance300ms to 600ms
Decorative loopDepends, 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.

translatescalerotateskew

8. Transform Origin

transform-origin controls the pivot point for transforms.

.door {
  transform-origin: left center;
}

.door:hover {
  transform: rotateY(18deg);
}

Common values:

  • center
  • top left
  • left center
  • bottom center
  • 50% 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:

  • opacity for fade,
  • transform for movement,
  • pointer-events so 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);
  }
}
0%50%100%startmiddleend

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;
}
PropertyMeaning
animation-nameWhich @keyframes to use
animation-durationHow long one cycle takes
animation-timing-functionSpeed curve
animation-delayWait before starting
animation-iteration-countNumber of times to run
animation-directionNormal, reverse, alternate
animation-fill-modeWhat styles apply before/after
animation-play-stateRunning 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 both forwards and backwards.

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.

border ringrotated over time

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;
}
card 1card 2card 380ms160ms240msEach card starts slightly later.

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:

  • transform
  • opacity
  • filter in small amounts
  • background-color
  • color
  • box-shadow in moderation

Avoid animating often:

  • width
  • height
  • top
  • left
  • right
  • bottom
  • margin
  • padding

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.

WorkMeaningExample trigger
LayoutRecalculate sizes and positionswidth, height, top
PaintRedraw pixelsbackground, box-shadow
CompositeMove already-painted layerstransform, opacity

For smooth UI motion, prefer composite-friendly properties.

LayoutPaintCompositeTransform and opacity usually cost less.

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:

  1. Motion should explain, not decorate.
  2. Keep common interactions fast.
  3. Use consistent timing across similar components.
  4. Animate transform and opacity when possible.
  5. Avoid large page-wide motion unless it has a purpose.
  6. Do not animate every element at once.
  7. Respect reduced-motion preferences.
  8. 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:

  1. Is the property animatable?
  2. Is the transition on the base state?
  3. Does the state actually change the property?
  4. Is the duration too short to notice?
  5. Is another rule overriding the transition or animation?
  6. Does the animation name match the @keyframes name exactly?
  7. Is animation-fill-mode needed?
  8. Is display: none preventing a transition?
  9. Is prefers-reduced-motion disabling motion?
  10. Are you animating expensive layout properties?
Check changed property and durationCheck selector state and source orderCheck keyframe name and fill modeCheck reduced motion and performance

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;
}