Episode 1 — Fundamentals / 1.12 — CSS Animations and Motion Design

1.12.d — Keyframe Animations

In one sentence: @keyframes lets you define multi-step timelines that run automatically — unlike transitions (which need a trigger), keyframe animations can loop, reverse, pause, and chain without any JavaScript.

Navigation: ← 1.12.c — 3D Transforms · 1.12.e — Animation Performance →


1. The @keyframes rule

@keyframes fadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}

Percentage steps for multi-stage animations

@keyframes bounce {
  0%   { transform: translateY(0); }
  40%  { transform: translateY(-30px); }
  60%  { transform: translateY(-15px); }
  80%  { transform: translateY(-5px); }
  100% { transform: translateY(0); }
}
  • from = 0%, to = 100%.
  • You can use any number of percentage stops.
  • Properties not mentioned at a stop are interpolated from the nearest defined stops.

2. The animation shorthand

/* animation: name  duration  timing  delay  count  direction  fill  play-state; */
   animation: fadeIn  500ms  ease-out   0s     1     normal   forwards  running;
Sub-propertyWhat it controlsCommon values
animation-nameWhich @keyframes to useany custom name
animation-durationTotal cycle time300ms, 1s
animation-timing-functionEasing curveease, linear, cubic-bezier(…)
animation-delayWait before first cycle0s, 200ms, -500ms (negative = start partway)
animation-iteration-countHow many cycles1, 3, infinite
animation-directionPlayback directionnormal, reverse, alternate, alternate-reverse
animation-fill-modeStyles applied before/afternone, forwards, backwards, both
animation-play-statePause/resumerunning, paused

3. animation-fill-mode — what happens before and after

  Timeline:   [delay] ──── [animation] ──── [after]
ValueDuring delayAfter final keyframe
noneElement's own stylesElement's own styles (snaps back)
forwardsElement's own stylesKeeps the to / 100% styles
backwardsApplies the from / 0% stylesElement's own styles
bothApplies from styles during delayKeeps to styles after end
.hero-text {
  opacity: 0;
  animation: fadeIn 600ms ease-out 200ms both;
  /* "both" → invisible during 200ms delay (from state),
     stays visible after animation ends (to state) */
}

4. animation-direction

ValueBehavior
normal0% → 100% every cycle
reverse100% → 0% every cycle
alternateOdd cycles: 0% → 100%; even cycles: 100% → 0%
alternate-reverseOdd cycles: 100% → 0%; even cycles: 0% → 100%

alternate is great for ping-pong effects like a pulsing glow:

.pulse {
  animation: glow 1.5s ease-in-out infinite alternate;
}
@keyframes glow {
  from { box-shadow: 0 0 5px rgba(59,130,246,0.5); }
  to   { box-shadow: 0 0 20px rgba(59,130,246,0.9); }
}

5. Infinite animations

.spinner {
  animation: spin 1s linear infinite;
}
@keyframes spin {
  to { transform: rotate(360deg); }
}

Caution: infinite animations run the compositor continuously. On mobile, this drains battery. Pause them when off-screen or when the user requests reduced motion.


6. Multiple animations

Comma-separate them — they run simultaneously:

.entrance {
  animation: fadeIn 400ms ease-out both,
             slideUp 400ms ease-out both;
}
@keyframes fadeIn {
  from { opacity: 0; }
  to   { opacity: 1; }
}
@keyframes slideUp {
  from { transform: translateY(20px); }
  to   { transform: translateY(0); }
}

Use different delays to stagger:

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

7. Practical CSS-only patterns

Loading spinner

.loader {
  width: 40px;
  height: 40px;
  border: 4px solid #e2e8f0;
  border-top-color: #3b82f6;
  border-radius: 50%;
  animation: spin 0.8s linear infinite;
}
@keyframes spin {
  to { transform: rotate(360deg); }
}

Fade-in on load

.fade-in {
  animation: fadeIn 500ms ease-out both;
}
@keyframes fadeIn {
  from { opacity: 0; transform: translateY(10px); }
  to   { opacity: 1; transform: translateY(0); }
}

Slide-in from left

.slide-in {
  animation: slideInLeft 400ms ease-out both;
}
@keyframes slideInLeft {
  from { transform: translateX(-100%); opacity: 0; }
  to   { transform: translateX(0);     opacity: 1; }
}

Pulse (attention)

.pulse {
  animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
  0%, 100% { transform: scale(1); }
  50%      { transform: scale(1.05); }
}

Bounce

.bounce {
  animation: bounce 0.6s ease-out;
}
@keyframes bounce {
  0%   { transform: translateY(0); }
  30%  { transform: translateY(-20px); }
  50%  { transform: translateY(-10px); }
  70%  { transform: translateY(-5px); }
  100% { transform: translateY(0); }
}

8. Negative delay trick

A negative delay makes the animation start partway through its timeline:

.progress-bar {
  animation: fillBar 3s linear both;
  animation-delay: -1.5s;  /* starts at 50% of the timeline */
}

Useful for staggered loading states where you want each item at a different phase.


9. Controlling animation with JS

element.style.animationPlayState = 'paused';   // pause
element.style.animationPlayState = 'running';   // resume

Listen for lifecycle events:

EventWhen it fires
animationstartFirst cycle begins (after delay)
animationiterationA new cycle starts (not the first)
animationendAll cycles complete
animationcancelAnimation removed or element hidden

10. Key takeaways

  1. @keyframes defines the what; animation applies the how (duration, easing, count, etc.).
  2. fill-mode: both is your friend — it holds styles during delay and after completion.
  3. alternate + infinite creates smooth back-and-forth loops.
  4. Stagger multiple elements with animation-delay offsets.
  5. Always animate transform and opacity for best performance.

Explain-It Challenge

Explain without notes:

  1. Why animation-fill-mode: forwards is needed to keep a fade-in visible after the animation ends.
  2. How animation-direction: alternate changes what animation-iteration-count: infinite looks like.
  3. The difference between a CSS transition and a CSS keyframe animation — name at least three differences.

Navigation: ← 1.12.c — 3D Transforms · 1.12.e — Animation Performance →