Home / Articles / Animation & Motion /

animation

Animation direction: normal, reverse, alternate

Control playback direction of keyframe animations. Use alternate for seamless bounces, reverse for reversed replays.

The direction property values

The animation-direction property controls how an animation plays through its keyframes. There are four possible values: normal, reverse, alternate, and alternate-reverse.

@keyframes move {
  from { transform: translateX(0); }
  to { transform: translateX(100px); }
}

.normal {
  animation: move 1s;
  animation-direction: normal; /* Default, plays 0% → 100% */
}

.reverse {
  animation: move 1s;
  animation-direction: reverse; /* Plays 100% → 0% */
}

.alternate {
  animation: move 1s;
  animation-direction: alternate; /* Forward, then backward, repeating */
  animation-iteration-count: infinite;
}

.alternate-reverse {
  animation: move 1s;
  animation-direction: alternate-reverse; /* Backward, then forward, repeating */
  animation-iteration-count: infinite;
}

normal: the default

normal is the default direction. Each iteration plays from the first keyframe (0%) to the last keyframe (100%). For one-shot animations like entrances, this is the expected behavior.

/* Standard entrance animation */
@keyframes slideIn {
  from { transform: translateY(20px); opacity: 0; }
  to { transform: translateY(0); opacity: 1; }
}
.entry {
  animation: slideIn 0.4s ease forwards;
  /* animation-direction: normal is implied */
}

When animation-direction is omitted, the animation plays normally on each iteration. Combined with animation-iteration-count: infinite, the animation jumps back to the start at the end of each cycle.

reverse: playing backward

Using reverse plays the animation backward: from 100% to 0%. This is useful when you need to "undo" an animation or create a mirrored effect.

/* Reversed replay of the same animation */
.original {
  animation: move 1s;
}
.reverse-version {
  animation: move 1s;
  animation-direction: reverse; /* Starts at 100%, ends at 0% */
}

/* Or use both directions with different delays */
.forward {
  animation: move 1s;
  animation-delay: 0s;
}
.backward {
  animation: move 1s;
  animation-direction: reverse;
  animation-delay: 1s; /* Starts after forward ends */
}

Note that reverse affects the playback direction but keeps the same timing function. If you're using ease-in (starts slow, accelerates), the reversed version will still use the same timing curve — just backward, making it start fast and slow down.

alternate: seamless bouncing

The alternate value is the most commonly used directional modifier. It plays forward on odd iterations (1, 3, 5...) and backward on even iterations (2, 4, 6...). This creates a seamless bounce effect.

/* Continuous bouncing effect */
@keyframes bounce {
  0%, 100% { transform: translateY(0); }
  50% { transform: translateY(-20px); }
}

.ball {
  animation: bounce 0.8s ease-in-out infinite;
  animation-direction: alternate; /* Smooth back-and-forth */
}

/* Without alternate, would jump back to 0% instantly */

The key benefit of alternate is that there's no sudden jump at cycle boundaries. Element transitions smoothly from end back to beginning, creating a continuous oscillation that's ideal for loaders, beacons, breathing effects.

alternate-reverse: starting from the end

alternate-reverse plays backward first, then forward on subsequent iterations. This is useful when you want the same bouncing effect but starting from the opposite end.

/* Reverse bounce starts from the end position */
@keyframes scan {
  0% { transform: translateX(0); }
  100% { transform: translateX(100%); }
}

.scan-left {
  animation: scan 2s linear infinite;
  animation-direction: alternate; /* 0% → 100% → 0% ... */
}

.scan-right {
  animation: scan 2s linear infinite;
  animation-direction: alternate-reverse; /* 100% → 0% → 100% ... */
}

Think of it this way: alternate always starts by going forward; alternate-reverse always starts by going backward. The difference is only visible on the first iteration.

Combining with timing functions

When using alternate, the timing function applies to each half of the cycle. For seamless oscillation, use ease-in-out which starts and ends slowly.

/* Ideal for alternate: smooth acceleration and deceleration */
@keyframes pulse {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.1); }
}

.ping {
  animation: pulse 1s ease-in-out infinite alternate;
  /* Smooth: small → large → small → large ... */
}

/* Avoid ease-out or ease-in alone: creates jerky reversals */

For linear motion requiring constant speed, use linear. But for most alternate animations, ease-in-out provides the smoothest feel at turning points.

Practical patterns

Common uses for each direction value:

/* 1. Alert pulse: alternate bounce */
@keyframes alertPulse {
  0%, 100% { box-shadow: 0 0 0 oklch(0.5 0.2 250 / 0); }
  50% { box-shadow: 0 0 20px oklch(0.5 0.2 250 / 0.3); }
}
.alert {
  animation: alertPulse 1s ease-in-out infinite alternate;
}

/* 2. Scanning underline: alternate horizontal */
@keyframes scanLine {
  from { transform: translateX(-100%); }
  to { transform: translateX(100%); }
}
.highlight {
  background: linear-gradient(90deg, oklch(0.5 0.2 250), transparent);
  animation: scanLine 1.5s ease-in-out infinite alternate;
}

/* 3. Loading bar oscillation: alternate width */
@keyframes loadingBar {
  0%, 100% { width: 10%; }
  50% { width: 90%; }
}
.loader-bar {
  height: 4px;
  background: oklch(0.5 0.2 250);
  animation: loadingBar 1.2s ease-in-out infinite alternate;
}

Interaction with iteration-count

When animation-iteration-count is not infinite with an alternating direction, the animation stops mid-cycle. The element may not return to its initial state.

/* 2.5 iterations: ends halfway back */
.partial {
  animation: move 1s alternate 2.5;
  /* Iteration 1: 0% → 100%
     Iteration 2: 100% → 0%
     Iteration 3 (partial): 0% → 50%
  Ends at 50% position */
}

This can be intentional for partial emphasis animations. But for predictable results, either use integer iteration counts or apply fill-mode: forwards and accept the final position as-is.

Accessibility: prefers-reduced-motion

Always provide reduced-motion alternatives that disable or minimize alternating animations. The continuous bouncing can cause discomfort for users with vestibular disorders.

@media (prefers-reduced-motion: reduce) {
  .alternative {
    animation: none;
  }
}

/* Or replace oscillating animation with static state */
.alternate {
  animation: pulse 1s ease-in-out infinite alternate;
}
@media (prefers-reduced-motion: reduce) {
  .alternate {
    opacity: 0.7; /* Static alternative */
  }
}

Combining animation-direction with animation-iteration-count

The behavior of animation-direction becomes particularly interesting when paired with specific iteration counts. With alternate or alternate-reverse, non-integer iteration counts create partial cycles that can produce unexpected final states. A count of 1.5 with alternate completes one full forward cycle, then half a backward cycle.

For predictable patterns, match iteration counts to your use case. Two iterations with alternate returns to the starting state—ideal for a single "bounce" effect. Three iterations leaves the element at the opposite end. Planning the exact count matters when animations aren't infinite, such as hover-triggered emphasis effects or one-time feedback animations.

/* Single complete bounce cycle: starts and ends at rest */
@keyframes hoverPop {
  from { transform: scale(1); }
  to { transform: scale(1.1); }
}
.hover-pop {
  animation: hoverPop 0.3s ease-out 1 alternate;
  /* Iteration 1: 1 → 1.1
     Iteration 2: 1.1 → 1
  Returns to scale(1) */
}

/* Triple-beat emphasis: three rapid pulses */
@keyframes emphasis {
  0%, 100% { transform: scale(1); }
  50% { transform: scale(1.15); }
}
.triple-beat {
  animation: emphasis 0.2s ease-in-out 3 alternate;
  /* Forward-back-forward-back-forward-back: 6 half-cycles */
}

animation-direction and animation-fill-mode interaction

When animation-direction combines with fill-mode, the final state depends on which iteration ends the animation. With forwards, the element retains the properties from the last keyframe reached—not necessarily 100% or 0%. This matters more with alternating directions where the endpoint varies.

With alternate and an even iteration count, the animation ends at 0%. With an odd count, it ends at 100%. Using fill-mode: forwards means the element "freezes" at whichever position the final iteration reaches. Design with this knowledge to avoid elements stuck in awkward intermediate states.

/* Odd iteration: element frozen at 100% (end) position */
@keyframes slide {
  from { transform: translateX(0); }
  to { transform: translateX(200px); }
}
.slide-odd {
  animation: slide 1s alternate 3 forwards;
  /* Ends at translateX(200px) */
}

/* Even iteration: element frozen at 0% (start) position */
.slide-even {
  animation: slide 1s alternate 2 forwards;
  /* Ends at translateX(0) */
}

/* For reverse direction with forwards, remember it starts at 100% */
.reverse-forwards {
  animation: slide 1s reverse 1 forwards;
  /* Ends at translateX(0) because reverse ends at 0% */
}

Practical UI patterns: progress bars with directional playback

Progress indicators often benefit from directional control. An indeterminate progress bar can use alternate to shuttle back and forth, signaling "still loading" without implying completion. The key is ensuring the animation loop has no visible snap point.

For loading skeletons, alternating width or position creates subtle motion that draws attention without being jarring. The alternate direction ensures smooth turns, while ease-in-out timing prevents abrupt velocity changes. This works especially well for content placeholders that load at variable speeds.

/* Indeterminate progress bar: alternating shuttle */
@keyframes indeterminateShuttle {
  0%, 100% { transform: translateX(-100%); }
  50% { transform: translateX(100%); }
}
.progress-indeterminate {
  position: relative;
  height: 4px;
  background: oklch(0.7 0.15 250);
  overflow: hidden;
}
.progress-indeterminate::after {
  content: "";
  position: absolute;
  left: 0;
  top: 0;
  bottom: 0;
  width: 40%;
  background: oklch(0.6 0.2 250);
  animation: indeterminateShuttle 1.5s ease-in-out infinite alternate;
}

/* Skeleton loading pulse with width oscillation */
@keyframes skeletonPulse {
  0%, 100% { opacity: 0.4; }
  50% { opacity: 0.8; }
}
.skeleton-text {
  height: 1rem;
  background: oklch(0.85 0 0);
  border-radius: 0.25rem;
  animation: skeletonPulse 1.2s ease-in-out infinite alternate;
}

Ping indicators and breathing effects

Live status indicators use alternate for that heartbeat rhythm. A green dot that softly expands and contracts signals activity without flashing. The breathing effect relies on smooth opacity or transform changes that feel natural, not mechanical.

Ping animations extend this concept outward—the indicator scales while fading, creating a ripple effect. Using alternate ensures the ripple collapses back smoothly before restarting. This continuous motion is more organic than a discrete loop that snaps back to the start.

/* Breathing status dot */
@keyframes breathingDot {
  0%, 100% { transform: scale(1); opacity: 0.7; }
  50% { transform: scale(1.2); opacity: 1; }
}
.status-dot {
  width: 0.5rem;
  height: 0.5rem;
  border-radius: 50%;
  background: oklch(0.7 0.2 150);
  animation: breathingDot 2s ease-in-out infinite alternate;
}

/* Ping ripple effect */
@keyframes pingRipple {
  0%, 100% {
    transform: scale(0.8);
    opacity: 0;
  }
  50% {
    transform: scale(1.3);
    opacity: 0.6;
  }
}
.ping-indicator {
  position: relative;
  display: inline-block;
}
.ping-indicator::before {
  content: "";
  position: absolute;
  inset: -0.5rem;
  border-radius: 50%;
  background: oklch(0.7 0.2 150 / 0.3);
  animation: pingRipple 2s ease-in-out infinite alternate;
}

Performance considerations for directional animations

Browser performance with animation-direction is comparable regardless of which value you choose. The critical factor is using transform and opacity properties, which can be GPU-accelerated. Avoid animating width, height, or margin in direction-changing animations, especially with alternate that runs continuously.

With alternate and infinite iteration counts, the animation never stops ticking. Ensure the keyframes use compositable properties to prevent layout thrashing. For complex animations, consider using will-change strategically, but only on elements that are actively animating.

/* Efficient: transform and opacity only */
@keyframes efficientAlternate {
  0%, 100% {
    transform: translateY(0);
    opacity: 1;
  }
  50% {
    transform: translateY(-10px);
    opacity: 0.8;
  }
}
.efficient {
  will-change: transform, opacity;
  animation: efficientAlternate 1s ease-in-out infinite alternate;
}

/* Inefficient: triggers layout recalculation each frame */
@keyframes inefficientAlternate {
  0%, 100% { margin-bottom: 0; }
  50% { margin-bottom: 20px; }
}
.inefficient {
  animation: inefficientAlternate 1s ease-in-out infinite alternate;
  /* Causes layout thrashing on every alternate cycle */
}

Summary

To control animation direction effectively:

  • Use normal (default) for one-shot animations that should play forward.
  • Use reverse for intentional backward playback of the same keyframes.
  • Use alternate for seamless oscillating effects (loaders, pulses, scans).
  • Use alternate-reverse when you need the same oscillation but starting from the end position.
  • Always pair alternate with ease-in-out for smooth turning points.
  • Plan iteration counts carefully—non-integer values leave elements at intermediate states.
  • Understand how fill-mode: forwards freezes at the actual endpoint, not a "normal" 100% position.
  • Use directional animations for realistic UI patterns: progress shuttles, breathing status dots, ping ripples.
  • Prioritize performance by animating transform and opacity, not layout properties.
  • Provide reduced-motion alternatives that remove or pause alternating animations.

Handling reduced motion with directional animations

Infinite alternating animations are among the most problematic patterns for users with vestibular disorders or motion sensitivity. A continuously pulsing badge or a perpetually shuttling progress bar can cause genuine physical discomfort. The prefers-reduced-motion: reduce media query is the right tool for addressing this, but the implementation details matter. Simply removing the animation entirely can break UI semantics — a loading indicator that disappears leaves users wondering if the page has frozen.

A better approach is to swap the animation for a static visual cue that conveys the same meaning without motion. An infinite opacity pulse can become a static opacity value that communicates "active". An alternating scale animation on a live-status dot can become a solid dot with a different color. For progress bars, consider replacing continuous motion with a stepped or static fill that still signals ongoing work.

/* Default: alternating pulse for activity */
@keyframes activePulse {
  from { opacity: 0.5; transform: scale(1); }
  to { opacity: 1; transform: scale(1.1); }
}
.status-indicator {
  background: oklch(0.65 0.22 145);
  animation: activePulse 1.5s ease-in-out infinite alternate;
}

/* Reduced motion: static but still visually distinct */
@media (prefers-reduced-motion: reduce) {
  .status-indicator {
    animation: none;
    opacity: 0.85;
    /* Slightly different hue signals active state without motion */
    background: oklch(0.72 0.25 145);
    outline: 2px solid oklch(0.55 0.2 145);
  }
}

This pattern — animation for those who can see it, clear static state for those who cannot — is more inclusive than either always-animate or never-animate. Test your reduced-motion fallbacks with the OS setting enabled (System Preferences → Accessibility on macOS, or Display → Remove animations on Windows) to verify the non-animated state still communicates clearly.