CSS steps() for Frame-by-Frame Sprite Animations

This article explains how the CSS property animation-timing-function: steps(4, jump-end) enables retro, stop-motion, and frame-by-frame sprite sheet animations. By replacing continuous transitions with abrupt, calculated shifts, the steps() timing function prevents the browser from smoothly interpolating movement. You will learn the mechanics behind sprite-based positioning, how the parameter values break down, and how to implement a fully functioning four-frame animation.

The Problem with Continuous Timing Functions

In standard CSS animations, timing functions like ease, linear, or cubic-bezier() calculate intermediate visual states between keyframes. If you animate a sprite sheet using linear, the browser smoothly slides the image across the view area. This produces an unwanted scrolling or smearing effect rather than the illusion of distinct, traditional animation frames.

Sprite animations require instantaneous cuts from one frame to the next.

Deconstructing steps(4, jump-end)

The steps() function turns an animation into a staircase progression instead of a smooth curve. It takes two primary parameters: the number of steps and the step position behavior.

1. The Step Count: 4

The integer 4 defines the number of equal intervals dividing the total animation duration. If an animation runs for 1 second, a step count of 4 splits the timeline into four distinct 0.25-second intervals. The element will display exactly four distinct positions corresponding to the four sprite frames.

2. The Direction Term: jump-end

The second parameter controls precisely when the jump between values occurs within each interval.

The jump-end keyword specifies that the visual change happens at the end of each time interval:

  • Step 1 (0.00s – 0.25s): Displays the initial state (Frame 1). At 0.25s, it abruptly jumps to Frame 2.
  • Step 2 (0.25s – 0.50s): Displays Frame 2. At 0.50s, it abruptly jumps to Frame 3.
  • Step 3 (0.50s – 0.75s): Displays Frame 3. At 0.75s, it abruptly jumps to Frame 4.
  • Step 4 (0.75s – 1.00s): Displays Frame 4. At 1.00s, the cycle finishes and loops back to Frame 1.

jump-end ensures that the starting frame is visible right away at 0% and that each frame remains static on screen for the full duration of its segment. (Note: jump-end is the modern standard syntax for the legacy end keyword).

Implementation Example

To create the animation, place a sprite sheet containing four equal-width frames inside an element sized to display only one frame at a time, hiding any overflow.

HTML

<div class="character"></div>

CSS

.character {
  width: 100px;
  height: 100px;
  background-image: url('spritesheet-400x100.png');
  background-repeat: no-repeat;
  background-position: 0 0;
  animation: play-sprite 0.8s steps(4, jump-end) infinite;
}

@keyframes play-sprite {
  from {
    background-position: 0 0;
  }
  to {
    background-position: -400px 0;
  }
}

How the Mechanics Work Together

  1. Sprite Sizing: The container element is set to 100px wide, matching the width of a single frame. The complete horizontal sprite image is 400px wide (4 frames × 100px).
  2. The Keyframes: The @keyframes rule shifts the background-position from 0 to -400px, which is the total width of the image.
  3. The Step Division: steps(4, jump-end) divides the -400px movement into four increments of -100px.
  4. Execution: The browser renders the background at 0px, then instantly snaps to -100px, -200px, and -300px at each quarter-interval of the animation duration. Because the movement is instantaneous, the user perceives a seamless, frame-by-frame animation without motion blur.