What steps() Actually Does

Most CSS timing functions โ€” ease, linear, ease-in-out โ€” interpolate smoothly between keyframe values. steps() is different: it divides the animation into a fixed number of equal jumps with no interpolation between them. The value snaps instantly from one step to the next.

linear
steps(6)
linear fills smoothly ยท steps(6) jumps in 6 discrete increments

The syntax is steps(count, jump-term) where count is the number of steps and jump-term controls where the snap happens. The default jump term is end.

Sprite Sheet Animation

A sprite sheet is a single image containing every frame of an animation laid out in a strip (horizontal or vertical). You show one frame at a time by shifting background-position in discrete steps.

Here's the core pattern:

CSS
/* 10-frame sprite sheet, each frame is 64ร—64px */
/* Total sheet width: 10 ร— 64 = 640px */

.sprite {
  width: 64px;
  height: 64px;
  background: url('run-cycle.png') left center;
  animation: play 0.8s steps(10) infinite;
}

@keyframes play {
  to { background-position: -640px 0; }
}

The critical relationship: step count = number of frames, and background-position shift = frames ร— frame width. Get either wrong and your animation breaks.

Why Frames Stack on Top of Each Other

This is the #1 question developers hit with step animations. The sprite frames visually overlap or smear instead of showing one clean frame at a time. There are three common causes:

1. Missing steps() โ€” using linear or ease instead

Without steps(), the browser smoothly slides the background position, causing every intermediate position (including positions between frames) to be visible simultaneously. The fix: use steps(N) where N is your frame count.

CSS โ€” WRONG
/* โŒ Smooth interpolation = frames slide and overlap */
animation: play 0.8s linear infinite;
CSS โ€” CORRECT
/* โœ… Discrete jumps = one clean frame at a time */
animation: play 0.8s steps(10) infinite;

2. Step count doesn't match frame count

If your sheet has 10 frames but you use steps(9), the last step will show a position partway between frames 9 and 10 โ€” producing an ugly half-frame. If you use steps(11), one step will show empty space past the end of the sheet.

๐Ÿ”‘ The Rule

steps(N) must equal the exact number of frames in your sprite sheet. Count them. Don't guess.

3. background-position shift doesn't equal frames ร— width

If each frame is 64px wide and you have 10 frames, the total shift must be exactly -640px. A shift of -600px or -100% (which depends on the element's width, not the background image's width) will misalign frames.

CSS
/* If your sprite has 8 frames at 100px each: */

/* โŒ Wrong โ€” 100% is relative to element width, not sprite */
@keyframes play { to { background-position: -100% 0; } }

/* โœ… Correct โ€” use exact pixel value */
@keyframes play { to { background-position: -800px 0; } }

The Four Jump Terms

The optional second parameter in steps() controls exactly when the jump happens within each step interval. This is where most confusion lives.

steps(n, end) โ€” the default

The value stays at the start of each interval and jumps at the end. This means the animation starts showing the first frame immediately and never shows the final value โ€” it jumps to the final value only at the very end (which in a looping animation, resets instantly). This is why it's the standard for sprite sheets: frame 1 is visible immediately, and the wrap-around to frame 1 happens seamlessly.

steps(n, start)

The value jumps immediately at the start of each interval. The first frame is skipped โ€” the animation begins by jumping to the second value. Use this for typewriter effects where you want the cursor to move forward immediately.

steps(n, jump-none)

Neither the first nor last value is skipped. The animation holds at both the 0% and 100% keyframe values. The number of visible stops is n + 1, so use steps(n-1, jump-none) if you want exactly n frames. This is useful when both the start and end state must be visible for a specific duration.

steps(n, jump-both)

Both the first and last values get a pause. The total number of output stops is n + 1. This is rare in practice but useful for animations that need a deliberate hold at each endpoint.

The two shorthand keywords step-start and step-end are equivalent to steps(1, start) and steps(1, end) โ€” they flip between two states with no intermediate. Useful for visibility toggling and clock-second ticking.

Typewriter Effect with steps()

A classic CSS-only effect. The idea: animate width from 0 to the text's full width in discrete character-sized steps, with overflow: hidden revealing one character at a time.

CSS
.typewriter {
  font-family: monospace;
  overflow: hidden;
  white-space: nowrap;
  border-right: 2px solid;
  width: 0;
  animation:
    type 2s steps(20) forwards,
    blink 0.7s step-end infinite;
}

@keyframes type {
  to { width: 20ch; }  /* 20 characters */
}

@keyframes blink {
  50% { border-color: transparent; }
}

The step count (20) matches the character count, and ch units ensure each step reveals exactly one character. Note the blinking cursor uses step-end โ€” a binary flip between visible and transparent.

Debugging Checklist

When your step animation isn't working, walk through these in order:

  1. Are you using steps() at all? Check that your animation-timing-function is steps(N), not ease or linear.
  2. Does N match your frame count? Open the sprite sheet in an image editor. Count the frames. Set steps(N) to that exact number.
  3. Is the background-position shift correct? It must equal N ร— frame-width in pixels. Don't use percentages โ€” they're relative to element size, not image size.
  4. Is the element the same size as one frame? The element's width and height must match a single frame's dimensions exactly.
  5. Are you animating the right direction? For a horizontal strip, animate background-position on the X axis only. For vertical, Y axis only.
  6. Is background-size set? If you've set background-size, it changes how percentages and auto-sizing work. Use the default or set it to the sprite sheet's actual dimensions.
  7. Check the jump term. If using jump-none, you need steps(N-1, jump-none) for N frames because both endpoints count as visible stops.
๐Ÿ’ก Pro Tip

Use the CSSTools.io Animation Generator to preview step-based easing in real time. Select steps(N) from the easing dropdown and adjust the step count with the slider to dial in the exact timing.

Try the Animation Generator

Preview steps(), ease, spring and 16 keyframe presets live. Copy production-ready CSS in one click.

Open Animation Generator โ†’