Most CSS animations are decorative. They happen, things move, users notice for a fraction of a second, and then the animation is over and forgotten. That's not necessarily bad — sometimes decoration is the point. But it's a waste of one of the most powerful communication tools in frontend development.

Animation can tell stories. It can communicate hierarchy, confirm actions, guide attention, and create an emotional relationship between a user and an interface. The difference between an animation that does this and one that doesn't comes down to a handful of fundamentals that most people skip.

The Problem With Transitions

Transitions are where most developers stop. Hover over an element, colour changes smoothly. Click a button, opacity fades. These are fine. But transitions are fundamentally reactive — they respond to state changes between two fixed points. They can't express personality. They can't tell you something is alive.

Keyframes are where animation becomes expressive. When you define the movement through multiple points in time, you can build in overshoot, settle, pulse, breathe — the qualities that make something feel physical rather than digital.

Easing Is Everything

The single most overlooked aspect of CSS animation is easing. Most developers reach for ease, ease-in-out, or — the worst — linear and call it done. But easing is how you define the personality of a movement. It's the difference between something that feels mechanical and something that feels natural.

Here's the same movement with different easing functions:

linear — mechanical
cubic-bezier — physical

The second one uses cubic-bezier(0.34, 1.56, 0.64, 1) — a slightly overshooting curve that gives the movement a sense of physical momentum. It lands, bounces fractionally, and settles. Nothing about this is technically complex. It's just one line of CSS. But it transforms the feeling completely.

The cubic-bezier values you should have memorised:

/* Snappy entrance — great for menus, modals, tooltips */
cubic-bezier(0.22, 1, 0.36, 1)

/* Spring overshoot — for interactive feedback, buttons */
cubic-bezier(0.34, 1.56, 0.64, 1)

/* Smooth exit — elements leaving the screen */
cubic-bezier(0.4, 0, 1, 1)

/* Dramatic entrance — for hero elements, big reveals */
cubic-bezier(0.16, 1, 0.3, 1)

Stagger: The Secret Weapon

Staggering animation delays across a group of elements is the difference between a list that appears and a list that arrives. Each element enters with a small delay after the previous one, creating a cascading wave that guides the eye and communicates sequence.

staggered delay — 150ms between each

In CSS, this is dead simple with custom properties:

/* Parent */
.list-item {
  animation: fade-up 0.5s cubic-bezier(0.22, 1, 0.36, 1) both;
  animation-delay: calc(var(--index) * 80ms);
}

/* In HTML: <li style="--index: 0">, <li style="--index: 1"> etc. */

The Three Rules of Purposeful Animation

1. Every animation should do a job

Before adding any animation, ask: what does this tell the user? A loading spinner communicates waiting. A slide-in panel communicates spatial hierarchy (content came from somewhere). A shake animation communicates error. If you can't answer what the animation communicates, it's decoration — and decoration should be the last thing you add, not the first.

2. Entrance and exit should be asymmetric

Elements should enter slowly (giving the user time to read/process) and exit quickly (because the user has already decided to dismiss it). A modal that fades in over 300ms should fade out in 150ms. This matches how attention works — you need time to orient to something new, but once you're done with it, you want it gone.

/* Modal enter — slow, from slightly below */
@keyframes modal-enter {
  from { opacity: 0; transform: translateY(12px) scale(0.97); }
  to   { opacity: 1; transform: translateY(0) scale(1); }
}

/* Modal exit — fast, shrinks away */
@keyframes modal-exit {
  from { opacity: 1; transform: scale(1); }
  to   { opacity: 0; transform: scale(0.95); }
}

.modal.entering { animation: modal-enter 280ms cubic-bezier(0.22, 1, 0.36, 1); }
.modal.exiting  { animation: modal-exit  160ms ease-in forwards; }

3. Respect reduced motion preferences

Not everyone can tolerate motion — vestibular disorders, epilepsy, and other conditions mean that for some users, animation isn't just annoying but actively harmful. Always wrap decorative animations in a prefers-reduced-motion media query:

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

Animation should serve the user, not impress them. The best animations are the ones people don't notice — they just feel right.

The Performance Checklist

Animation done well is invisible. Nobody watches a perfectly animated interface and thinks "wow, great animation." They just feel comfortable, oriented, and engaged. That's the goal — not to show off what CSS can do, but to make the interface feel alive in a way that serves the person using it.