Animation Properties and Timing

Controlling Animation with CSS Properties

While @keyframes define what happens during an animation, animation properties control how the animation behaves. These properties give you precise control over timing, repetition, direction, and many other aspects of your animations.

Understanding these properties is like knowing how to operate a professional video editing suite - you can fine-tune every aspect of the animation to achieve exactly the effect you want.

graph LR A[Animation Properties] --> B[Timing Properties] A --> C[Playback Properties] A --> D[State Properties] B --> E[duration] B --> F[delay] B --> G[timing-function] C --> H[iteration-count] C --> I[direction] C --> J[fill-mode] D --> K[play-state] D --> L[name]

Essential Animation Properties

animation-name

Specifies the name of the @keyframes rule to use.

.element {
  animation-name: bounce; /* Must match an existing @keyframes rule */
}

You can also specify none to disable animations on an element.

animation-duration

Defines how long one cycle of the animation takes to complete.

.element {
  animation-duration: 2s; /* 2 seconds */
}

.quick-animation {
  animation-duration: 200ms; /* 200 milliseconds */
}

The default duration is 0s, which means the animation happens instantly (no visible animation). Values are typically specified in seconds (s) or milliseconds (ms).

animation-timing-function

Controls the pace at which the animation plays by defining how intermediate values are calculated.

.element {
  animation-timing-function: ease; /* Default - slow start, fast middle, slow end */
}

The timing function greatly affects the feel of an animation. It's like the difference between a car accelerating smoothly versus one that jolts forward and brakes abruptly.

animation-delay

Specifies the amount of time to wait before starting the animation.

.element {
  animation-delay: 1s; /* Wait 1 second before starting */
}

.immediate-animation {
  animation-delay: 0s; /* Start immediately (default) */
}

.pre-animated {
  animation-delay: -0.5s; /* Start halfway through the animation */
}

Negative values are allowed and cause the animation to start as if it had already been playing for that amount of time.

animation-iteration-count

Defines how many times the animation should repeat.

.once {
  animation-iteration-count: 1; /* Play once (default) */
}

.twice {
  animation-iteration-count: 2; /* Play twice */
}

.forever {
  animation-iteration-count: infinite; /* Play indefinitely */
}

You can also use decimal values for partial iterations (e.g., 2.5 would play twice and stop halfway through the third cycle).

animation-direction

Controls whether the animation plays forward, backward, or alternates.

.normal {
  animation-direction: normal; /* Forward each cycle (default) */
}

.reverse {
  animation-direction: reverse; /* Backward each cycle */
}

.alternate {
  animation-direction: alternate; /* Forward then backward */
}

.alternate-reverse {
  animation-direction: alternate-reverse; /* Backward then forward */
}

The alternate options are particularly useful for creating pendulum-like effects or other oscillating animations.

animation-fill-mode

Specifies how the animation affects the element before and after it executes.

.none {
  animation-fill-mode: none; /* No styles applied before or after (default) */
}

.forwards {
  animation-fill-mode: forwards; /* Retain the style values from the last keyframe */
}

.backwards {
  animation-fill-mode: backwards; /* Apply values from the first keyframe during delay */
}

.both {
  animation-fill-mode: both; /* Apply both forwards and backwards behaviors */
}

This property is crucial for animations that need to leave elements in their final state, like elements that animate into the viewport and should stay there.

animation-play-state

Specifies whether the animation is running or paused.

.running {
  animation-play-state: running; /* Play the animation (default) */
}

.paused {
  animation-play-state: paused; /* Pause the animation */
}

This is often changed via JavaScript in response to user interactions, like pausing an animation when the user hovers over an element.

Timing Functions in Depth

Understanding Animation Timing

The animation-timing-function property controls the acceleration and deceleration of your animation. It's similar to how a car might accelerate and decelerate - not at a constant rate, but with varying speed.

Time Progress 0% 25% 50% 75% 100% linear ease ease-in ease-out ease-in-out

Standard Timing Functions

Using Cubic Bezier Curves

For precise control, you can define custom timing functions using cubic-bezier:

/* Format: cubic-bezier(x1, y1, x2, y2) */
.custom-timing {
  animation-timing-function: cubic-bezier(0.68, -0.55, 0.27, 1.55);
}

The values represent control points for a Bezier curve, where:

Tools like cubic-bezier.com allow you to visually design custom timing functions.

Using Steps Functions

For frame-by-frame animations or discrete steps:

.step-animation {
  animation-timing-function: steps(5, end);
}

The steps function creates a stair-step effect with a specified number of equal steps:

This is perfect for sprite-based animations or creating "ticker" effects like old flip clocks or mechanical counters.

Real-World Timing Examples

/* Bouncy effect with overshoot */
.bounce {
  animation-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275);
}

/* Quick acceleration, gradual deceleration */
.accelerate-then-coast {
  animation-timing-function: cubic-bezier(0.25, 0.1, 0.25, 1);
}

/* Elastic motion like a spring */
.elastic {
  animation-timing-function: cubic-bezier(0.68, -0.55, 0.265, 1.55);
}

/* Digital clock counter (10 digits) */
.digital-counter {
  animation-timing-function: steps(10, end);
}

Working with Animation Duration and Delay

Choosing the Right Duration

The duration of an animation significantly impacts how it's perceived:

Duration Perception Good for
Ultra-fast
(50-150ms)
Instantaneous but noticeable Button clicks, toggles, micro-interactions
Fast
(150-300ms)
Quick and responsive Interface feedback, hover effects
Medium
(300-500ms)
Smooth transition Page transitions, content changes
Slow
(500ms-1s)
Deliberate movement Entrance/exit animations, attention-grabbing elements
Very slow
(1s+)
Dramatic effect Storytelling, background effects, loading indicators

In general, shorter durations make interfaces feel more responsive, while longer durations can create more emotional impact but risk feeling sluggish if overused.

Strategic Use of Delays

Animation delays can create more complex, choreographed sequences:

Sequential Animations

/* Elements appear one after another */
.item:nth-child(1) { animation-delay: 0s; }
.item:nth-child(2) { animation-delay: 0.2s; }
.item:nth-child(3) { animation-delay: 0.4s; }
.item:nth-child(4) { animation-delay: 0.6s; }

This creates a domino or cascading effect, like items being placed on a table one after another.

Offset Animations

/* Elements animate with offset timing */
.circle-1 {
  animation: pulse 2s infinite;
}
.circle-2 {
  animation: pulse 2s 0.6s infinite;
}
.circle-3 {
  animation: pulse 2s 1.2s infinite;
}

This creates a wave-like effect, where animations overlap but aren't synchronized. It's like watching ripples in water that start at different times.

Negative Delays

/* Start animation from a midpoint */
.pre-animated {
  animation: slide-in 1s -0.5s both;
}

This makes the animation begin as if it had already been playing for the specified time. It's like joining a movie halfway through - you don't see the beginning.

Animation Iteration and Direction

Controlling Repetition

The animation-iteration-count property controls how many times an animation plays:

/* Play once and stop */
.once {
  animation-iteration-count: 1; /* Default */
}

/* Play exactly three times */
.three-times {
  animation-iteration-count: 3;
}

/* Play indefinitely */
.infinite {
  animation-iteration-count: infinite;
}

/* Play two and a half times */
.partial {
  animation-iteration-count: 2.5;
}

Infinite animations are useful for loading indicators or background effects. Specific counts are good for drawing attention (like three quick pulses) before returning to normal.

Animation Direction Patterns

The animation-direction property creates different patterns of repeated animations:

normal reverse alternate alternate-reverse

Normal Direction

.normal {
  animation-direction: normal; /* Default */
}

Each cycle of the animation plays from 0% to 100%. Like reading a book from beginning to end, then starting over at the beginning.

Reverse Direction

.reverse {
  animation-direction: reverse;
}

Each cycle plays from 100% to 0%. Like reading a book from end to beginning, then starting again from the end.

Alternate Direction

.alternate {
  animation-direction: alternate;
}

Odd cycles play from 0% to 100%, even cycles play from 100% to 0%. Like reading a book from beginning to end, then from end to beginning, then beginning to end again. This creates smooth back-and-forth movement, perfect for oscillating animations like a pendulum or breathing effect.

Alternate-Reverse Direction

.alternate-reverse {
  animation-direction: alternate-reverse;
}

Odd cycles play from 100% to 0%, even cycles play from 0% to 100%. Like reading a book from end to beginning, then from beginning to end, then end to beginning again.

Creating Continuous Loops

For seamless looping animations, you need to ensure the end state connects smoothly back to the start state. The combination of iteration count and direction affects this:

/* Seamless back-and-forth animation */
@keyframes slide {
  0% { transform: translateX(0); }
  100% { transform: translateX(100px); }
}

.pendulum {
  animation: slide 2s ease-in-out infinite alternate;
}

For circular or rotating animations, a simple 0-360 degree rotation with infinite count works well:

@keyframes rotate {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

.spinner {
  animation: rotate 2s linear infinite;
}

The linear timing function is often preferred for continuous rotations to maintain constant speed.

Fill Modes and Animation States

Understanding Fill Modes

The animation-fill-mode property controls what happens before an animation starts and after it ends. Think of it like configuring what happens when a movie isn't playing - does the screen go blank, freeze on the last frame, or show something else?

Animation Active Before After none forwards backwards both

None (Default)

.element {
  animation-fill-mode: none;
}

The element doesn't retain any animation styles before or after execution. It's like a theater where the stage is empty before the play starts and after it ends.

Forwards

.element {
  animation-fill-mode: forwards;
}

The element retains the computed values set by the last keyframe encountered during execution. It's like a movie that freezes on the final frame when it ends.

This is particularly useful for animations that move elements to a new position or state that should be maintained after the animation completes.

Backwards

.element {
  animation-fill-mode: backwards;
}

The element applies the values defined in the first relevant keyframe during the animation delay period. It's like an actor taking their starting position before the curtain rises.

This is helpful when you have a delayed animation and want the element to be in its starting state during the delay, rather than its default state.

Both

.element {
  animation-fill-mode: both;
}

The element will follow both forwards and backwards rules, extending the animation in both directions. It's like having actors in position before the play starts, and then freezing in their final position when it ends.

Common Use Cases for Fill Modes

/* Element fades in and stays visible */
.fade-in-stay {
  animation: fade-in 1s forwards;
  opacity: 0; /* Initial state before animation */
}

/* Element starts from offscreen position during delay */
.delayed-entrance {
  animation: slide-in 1s 2s backwards;
  /* No need to set initial transform, backwards handles it */
}

/* Element animates from start position to end position and stays there */
.complete-movement {
  animation: move-to-position 2s both;
}

Animation Play State

Controlling Playback

The animation-play-state property allows you to pause and resume animations:

.element {
  animation: bounce 1s infinite;
  animation-play-state: running; /* Default state */
}

.element.paused {
  animation-play-state: paused;
}

This is particularly useful when combined with user interactions:

/* Pause animation on hover */
.animated {
  animation: pulse 2s infinite;
}

.animated:hover {
  animation-play-state: paused;
}

When an animation is paused, it freezes in its current state. When resumed, it continues from exactly where it left off. This is like pausing a video - the frame freezes, and when you press play, it continues from that exact frame.

JavaScript Control of Animations

For more complex control, you can manipulate animation properties with JavaScript:

// JavaScript to toggle animation play state
const element = document.querySelector('.animated-element');
const pauseButton = document.querySelector('#pause-button');

pauseButton.addEventListener('click', function() {
  const currentState = window.getComputedStyle(element).animationPlayState;
  
  if (currentState === 'running') {
    element.style.animationPlayState = 'paused';
    pauseButton.textContent = 'Play';
  } else {
    element.style.animationPlayState = 'running';
    pauseButton.textContent = 'Pause';
  }
});

This allows for DVD player-like controls (play, pause, etc.) for your animations, giving users more control over animated content.

Multiple Animations on One Element

Applying Multiple Animations

You can apply multiple animations to a single element by using comma-separated values:

.multi-animated {
  animation: 
    fade-in 1s ease-out,
    slide-up 1.2s ease-in-out,
    pulse 2s ease infinite 1s;
}

Each comma-separated value represents a complete animation definition. This allows you to combine different effects, like having an element fade in while also sliding up from the bottom.

This is like a dancer performing multiple movements simultaneously - perhaps spinning while also moving across the stage and performing arm gestures.

Individual Property Control

You can also set individual properties for multiple animations:

.multi-animated {
  animation-name: fade-in, slide-up, pulse;
  animation-duration: 1s, 1.2s, 2s;
  animation-timing-function: ease-out, ease-in-out, ease;
  animation-delay: 0s, 0s, 1s;
  animation-iteration-count: 1, 1, infinite;
}

Note that properties with fewer values than animations will repeat. For example, if you have three animation names but only two durations, the third animation will use the first duration.

Practical Example: Complex Loading Animation

@keyframes rotate {
  from { transform: rotate(0deg); }
  to { transform: rotate(360deg); }
}

@keyframes pulse-opacity {
  0%, 100% { opacity: 0.6; }
  50% { opacity: 1; }
}

@keyframes pulse-size {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.1); }
}

.loader {
  width: 50px;
  height: 50px;
  border-radius: 50%;
  border: 3px solid transparent;
  border-top-color: #3498db;
  border-bottom-color: #3498db;
  
  /* Multiple animations applied */
  animation: 
    rotate 1.5s linear infinite,
    pulse-opacity 2s ease infinite,
    pulse-size 3s ease infinite;
}

This creates a complex loader that simultaneously rotates, pulses in opacity, and subtly changes size, creating a rich, dynamic effect from simple components.

Animation Performance Considerations

Properties to Animate

Not all CSS properties are created equal when it comes to animation performance. For the smoothest animations, focus on these properties:

graph TD A[CSS Properties] --> B[Composite Properties] A --> C[Layout Properties] A --> D[Paint Properties] B --> E[transform] B --> F[opacity] B --> G["Best for Animation
(GPU Accelerated)"] C --> H[width/height] C --> I[top/right/bottom/left] C --> J["Cause Layout Recalculation
(Can Be Slow)"] D --> K[color] D --> L[background-color] D --> M[box-shadow] D --> N["Cause Repainting
(Medium Performance)"]

When possible, use transform and opacity for animations. These properties can be handled by the GPU (graphics processing unit) and don't require layout recalculations.

/* Good: Uses transform for movement */
@keyframes good-animation {
  from { transform: translateX(0); }
  to { transform: translateX(200px); }
}

/* Avoid: Causes layout recalculations */
@keyframes avoid-animation {
  from { left: 0; }
  to { left: 200px; }
}

will-change Property

For complex animations, you can hint to the browser which properties will change:

.optimized-animation {
  will-change: transform, opacity;
  animation: slide-fade 1s;
}

However, use this property sparingly - overuse can actually harm performance. Only use it when you have a performance issue that needs solving.

Animation Timing

Be mindful of animation duration, especially for UI elements:

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

Real-World Animation Examples

Staggered List Items

@keyframes fade-in-up {
  from {
    opacity: 0;
    transform: translateY(20px);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.list-item {
  opacity: 0;
  animation: fade-in-up 0.5s ease-out forwards;
}

.list-item:nth-child(1) { animation-delay: 0.1s; }
.list-item:nth-child(2) { animation-delay: 0.2s; }
.list-item:nth-child(3) { animation-delay: 0.3s; }
.list-item:nth-child(4) { animation-delay: 0.4s; }
.list-item:nth-child(5) { animation-delay: 0.5s; }

This creates a cascading entrance effect for list items, drawing the user's attention naturally through the content. It's like watching items being placed on a table one after another.

Animated Notification Badge

@keyframes pulse-scale {
  0% {
    transform: scale(0);
    opacity: 0;
  }
  50% {
    transform: scale(1.2);
    opacity: 0.8;
  }
  100% {
    transform: scale(1);
    opacity: 1;
  }
}

.notification-badge {
  display: inline-block;
  background-color: #e74c3c;
  color: white;
  border-radius: 50%;
  padding: 0.25em 0.6em;
  font-size: 12px;
  
  /* Animation properties */
  animation: pulse-scale 0.3s cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
  transform-origin: center;
}

This creates an attention-grabbing effect for new notifications, with a slight "bounce" at the end thanks to the custom cubic-bezier timing function. It mimics how we might physically place an object down with a slight bounce.

Interactive Button with Multiple States

@keyframes button-hover {
  to {
    box-shadow: 0 5px 15px rgba(0,0,0,0.2);
    transform: translateY(-2px);
  }
}

@keyframes button-active {
  to {
    transform: translateY(1px);
    box-shadow: 0 2px 5px rgba(0,0,0,0.1);
  }
}

.button {
  background-color: #3498db;
  color: white;
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  box-shadow: 0 3px 10px rgba(0,0,0,0.1);
  transition: all 0.2s;
}

.button:hover {
  animation: button-hover 0.2s forwards;
}

.button:active {
  animation: button-active 0.1s forwards;
}

This creates a physically-inspired button that appears to rise up when hovered and press down when clicked, providing visual feedback that mimics real-world button behavior.

Practice Activity: Fine-tuning Animation Timing

Activity Instructions:

  1. Start with this basic animation example and experiment with different property combinations:
  2. Try different timing functions to see how they affect the "feel" of the animation
  3. Experiment with different iteration counts and directions
  4. Test different fill modes to see how they affect the element before and after animation
  5. Create a sequence of animations by using different delays

Starter Code:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Animation Timing Practice</title>
  <style>
    body {
      font-family: Arial, sans-serif;
      padding: 20px;
      max-width: 800px;
      margin: 0 auto;
    }
    
    .controls {
      margin-bottom: 20px;
      padding: 15px;
      background-color: #f5f5f5;
      border-radius: 5px;
    }
    
    .controls label {
      display: inline-block;
      margin-right: 10px;
      min-width: 150px;
    }
    
    .controls select, .controls input {
      padding: 5px;
      margin-bottom: 10px;
    }
    
    .animation-container {
      height: 300px;
      border: 1px solid #ddd;
      border-radius: 5px;
      position: relative;
      overflow: hidden;
    }
    
    .animated-box {
      width: 80px;
      height: 80px;
      background-color: #3498db;
      border-radius: 5px;
      position: absolute;
      top: 50%;
      left: 0;
      transform: translateY(-50%);
    }
    
    @keyframes move-right {
      from {
        left: 0;
      }
      to {
        left: calc(100% - 80px);
      }
    }
    
    /* Animation will be applied via JavaScript based on user selections */
  </style>
</head>
<body>
  <h1>Animation Timing Practice</h1>
  
  <div class="controls">
    <label for="timing-function">Timing Function:</label>
    <select id="timing-function">
      <option value="linear">linear</option>
      <option value="ease" selected>ease</option>
      <option value="ease-in">ease-in</option>
      <option value="ease-out">ease-out</option>
      <option value="ease-in-out">ease-in-out</option>
      <option value="cubic-bezier(0.68, -0.55, 0.27, 1.55)">bouncy (custom cubic-bezier)</option>
      <option value="steps(10, end)">steps(10, end)</option>
    </select>
    <br>
    
    <label for="duration">Duration (seconds):</label>
    <input type="number" id="duration" min="0.1" max="10" step="0.1" value="2">
    <br>
    
    <label for="delay">Delay (seconds):</label>
    <input type="number" id="delay" min="0" max="5" step="0.1" value="0">
    <br>
    
    <label for="iteration-count">Iteration Count:</label>
    <input type="number" id="iteration-count" min="1" max="10" value="1">
    <option value="infinite">Infinite</option>
    <br>
    
    <label for="direction">Direction:</label>
    <select id="direction">
      <option value="normal" selected>normal</option>
      <option value="reverse">reverse</option>
      <option value="alternate">alternate</option>
      <option value="alternate-reverse">alternate-reverse</option>
    </select>
    <br>
    
    <label for="fill-mode">Fill Mode:</label>
    <select id="fill-mode">
      <option value="none">none</option>
      <option value="forwards" selected>forwards</option>
      <option value="backwards">backwards</option>
      <option value="both">both</option>
    </select>
    <br>
    
    <button id="play-button">Play Animation</button>
    <button id="reset-button">Reset</button>
  </div>
  
  <div class="animation-container">
    <div class="animated-box"></div>
  </div>
  
  <script>
    document.addEventListener('DOMContentLoaded', function() {
      const box = document.querySelector('.animated-box');
      const playButton = document.getElementById('play-button');
      const resetButton = document.getElementById('reset-button');
      
      // Get all control elements
      const timingFunction = document.getElementById('timing-function');
      const duration = document.getElementById('duration');
      const delay = document.getElementById('delay');
      const iterationCount = document.getElementById('iteration-count');
      const direction = document.getElementById('direction');
      const fillMode = document.getElementById('fill-mode');
      
      // Apply animation based on current settings
      function applyAnimation() {
        // Clear any existing animation
        box.style.animation = 'none';
        
        // Force reflow
        void box.offsetWidth;
        
        // Set new animation
        const iterCount = iterationCount.value === 'infinite' ? 'infinite' : iterationCount.value;
        const animationString = `move-right ${duration.value}s ${timingFunction.value} ${delay.value}s ${iterCount} ${direction.value} ${fillMode.value}`;
        
        box.style.animation = animationString;
      }
      
      // Event listeners
      playButton.addEventListener('click', applyAnimation);
      
      resetButton.addEventListener('click', function() {
        box.style.animation = 'none';
        box.style.left = '0';
      });
    });
  </script>
</body>
</html>

Extension Tasks:

  1. Add a visual timeline that shows the animation's progress and highlights different phases
  2. Create a second animated element and make it follow the first with a delay
  3. Add controls for manipulating the animation-play-state property
  4. Implement a preset system that applies different combinations of properties for specific effects (e.g., "Bouncy", "Smooth", "Robotic")

Conclusion

Understanding animation properties and timing gives you precise control over how your animations behave. By combining these properties effectively, you can create animations that are not just visually appealing, but also enhance user experience through appropriate timing, smooth movement, and meaningful feedback.

In our next lesson, we'll explore techniques for optimizing animation performance to ensure your animations run smoothly across devices, even on less powerful hardware.