Home / Articles / Animation & Motion /
Animation play state: pausing animations
Pause, resume, and control animation playback with animation-play-state. Interactive diagrams, video controls, and more.
The play-state property
The animation-play-state property allows you to pause or resume a running animation. Values are running (default) or paused. The animation state toggles without resetting, resuming from the exact frame where it stopped.
/* Start paused, resume on hover */
.diagram {
animation: rotate 10s linear infinite;
animation-play-state: paused;
}
.diagram:hover {
animation-play-state: running;
}
/* Or start running, pause on hover */
.autoplay {
animation: move 2s ease-in-out infinite;
animation-play-state: running;
}
.autoplay:hover {
animation-play-state: paused;
}
Interactive diagram patterns
A common use case is an animated diagram that users can pause to examine details. The pause-on-hover pattern is intuitive and requires no JavaScript.
.step {
animation: fadeIn 0.3s ease forwards;
animation-delay: calc(var(--step) * 0.3s);
opacity: 0;
}
/* Container controls all steps */
.steps-container {
animation: sequence 2s linear infinite;
animation-play-state: paused;
}
.steps-container:hover,
.steps-container.is-playing {
animation-play-state: running;
}
@keyframes sequence {
0% { opacity: 1; }
80% { opacity: 1; }
100% { opacity: 0; }
}
The pause state preserves all animation progress. When the user hovers to check a detail, moving away resumes from that exact point rather than restarting.
Programmatically controlling play state
Use JavaScript to toggle play-state via class changes or direct style modifications. This is essential for accessible controls that pause animations on demand.
const button = document.querySelector('#play-pause');
const carousel = document.querySelector('.carousel');
let isPlaying = true;
button.addEventListener('click', () => {
if (isPlaying) {
carousel.style.animationPlayState = 'paused';
button.textContent = 'Play';
} else {
carousel.style.animationPlayState = 'running';
button.textContent = 'Pause';
}
isPlaying = !isPlaying;
});
/* Or toggle a class */
button.addEventListener('click', () => {
carousel.classList.toggle('is-playing');
});
Direct style manipulation with style.animationPlayState (camelCase) or class toggling both work. Class toggling is preferred for better style separation.
Accessibility and user control
Providing pause controls is an accessibility requirement for certain types of animation. WCAG 2.1 Success Criterion 2.2.2 (Pause, Stop, Hide) requires that moving, blinking, or scrolling content that starts automatically has a mechanism to pause, stop, or hide it.
Pausing animations on hover
The most common and intuitive way to pause an animation is on hover. This pattern requires zero JavaScript and works across all modern browsers. When users encounter animations that move too quickly to examine in detail, hovering provides an immediate stop mechanism. This is particularly valuable for technical diagrams, infographics, or any visual content where precision observation matters.
/* Technical diagram that pauses on hover */
.technology-diagram {
--primary: oklch(65% 0.22 260);
--secondary: oklch(75% 0.18 15);
--bg: oklch(14% 0.03 260);
width: 100%;
max-width: 400px;
aspect-ratio: 16 / 9;
position: relative;
background: var(--bg);
border-radius: 0.75rem;
overflow: hidden;
}
.diagram-element {
position: absolute;
width: 3rem;
height: 3rem;
border-radius: 0.5rem;
background: linear-gradient(135deg, var(--primary), oklch(45% 0.15 260));
animation: float 3s ease-in-out infinite;
animation-play-state: running;
}
.diagram-element:nth-child(1) {
left: 10%;
top: 20%;
animation-delay: 0s;
}
.diagram-element:nth-child(2) {
right: 15%;
top: 35%;
animation-delay: 0.5s;
}
.diagram-element:nth-child(3) {
left: 30%;
bottom: 25%;
animation-delay: 1s;
}
.technology-diagram:hover .diagram-element,
.technology-diagram:focus-within .diagram-element {
animation-play-state: paused;
}
@keyframes float {
0%, 100% {
transform: translateY(0) scale(1);
}
50% {
transform: translateY(-12px) scale(1.05);
}
}
The CSS :hover and :focus-within pseudo-classes ensure that both mouse and keyboard users can pause the animation. When hovering over any part of the container, all descendant elements stop at their current state. The animation retains its full timing function and will resume from exactly where it paused when the hover ends.
This pattern works exceptionally well with CSS custom properties for theming. By using oklch() color values, you can create accessible color combinations that maintain proper contrast ratios while achieving modern, vibrant aesthetics. The oklch() color space provides more perceptually uniform color transitions compared to traditional sRGB values.
JavaScript-controlled pause and resume
While hover works for simple cases, many scenarios require explicit user controls. Video players, carousels, and tutorials all benefit from visible play/pause buttons that provide clear affordance. JavaScript enables programmatic control over animation state, allowing you to build custom control interfaces that integrate with your existing UI components.
/* Animated progress visualization with controls */
.progress-visualization {
--track: oklch(22% 0.04 260);
--fill: oklch(65% 0.22 145);
width: 100%;
height: 4px;
background: var(--track);
border-radius: 2px;
position: relative;
overflow: hidden;
}
.progress-bar {
position: absolute;
top: 0;
left: 0;
height: 100%;
width: 75%;
background: var(--fill);
border-radius: 2px;
box-shadow: 0 0 12px oklch(65% 0.22 145 / 40%);
animation: pulse-width 2s ease-in-out infinite;
}
@keyframes pulse-width {
0%, 100% {
transform: scaleX(0.95);
}
50% {
transform: scaleX(1.05);
}
}
/* Control buttons */
.animation-controls {
display: flex;
gap: 0.5rem;
margin-top: 1rem;
}
.animation-controls button {
padding: 0.5rem 1rem;
background: oklch(28% 0.05 260);
color: oklch(92% 0.02 260);
border: 1px solid oklch(38% 0.04 260);
border-radius: 0.375rem;
cursor: pointer;
font-size: 0.875rem;
transition: background-color 0.15s ease;
}
.animation-controls button:hover {
background: oklch(35% 0.06 260);
}
.animation-controls button:focus {
outline: 2px solid oklch(65% 0.22 145);
outline-offset: 2px;
}
/* Playing state for JS toggling */
.progress-bar.is-paused {
animation-play-state: paused;
}
Implementing the play/pause toggle requires tracking the current state and updating both the animation and the UI. Use element.classList.toggle() to add or remove a CSS class that controls animation-play-state. This approach maintains a clean separation between presentation and behavior while keeping the CSS-driven animations intact.
For more complex scenarios with multiple animations, you may need to track each animation individually. Consider using the Web Animations API for finer control over individual animation instances, though for most use cases the class-based approach with animation-play-state remains the most maintainable solution.
Accessibility and prefers-reduced-motion
Animation pose control is not just a convenience feature—it's an accessibility requirement. WCAG 2.1 Success Criterion 2.2.2 mandates that moving, blinking, or scrolling content that starts automatically must have a mechanism to pause, stop, or hide it. This applies to any animation that persists longer than five seconds or runs in a loop.
/* Respect user motion preferences */
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.autoplay-spinner {
width: 3rem;
height: 3rem;
border-radius: 50%;
border: 3px solid oklch(35% 0.1 260);
border-top-color: oklch(65% 0.22 260);
animation: spin 1s linear infinite;
}
/* Respect prefers-reduced-motion */
@media (prefers-reduced-motion: reduce) {
.autoplay-spinner {
animation: none;
border-top-color: oklch(35% 0.1 260);
}
}
/* Provide accessible controls */
.accessible-spinner {
position: relative;
display: inline-block;
}
.accessible-spinner .spinner {
display: block;
animation: spin 1s linear infinite;
}
.accessible-spinner .spinner.paused {
animation-play-state: paused;
}
/* Accessible pause button */
.accessible-spinner button {
position: absolute;
bottom: -2.5rem;
left: 50%;
transform: translateX(-50%);
padding: 0.375rem 0.75rem;
font-size: 0.75rem;
background: oklch(22% 0.04 260);
color: oklch(92% 0.02 260);
border: 1px solid oklch(35% 0.05 260);
border-radius: 0.25rem;
cursor: pointer;
}
.accessible-spinner button:focus {
outline: 2px solid oklch(65% 0.22 260);
outline-offset: 2px;
}
.accessible-spinner button[aria-pressed="true"] {
background: oklch(45% 0.08 260);
}
The prefers-reduced-motion media query detects when users have requested reduced motion in their system settings. Always check for this preference before starting automatic animations and provide alternatives. You can either completely disable the animation, reduce its speed, or provide prominent pause controls.
For users who rely on screen readers or cognitive accessibility, continuous animations can be distracting or even trigger motion sensitivity. Implement a two-layer strategy: respect prefers-reduced-motion by default, and always provide visible, keyboard-accessible pause controls regardless of the user's system preferences. Use aria-pressed attributes on toggle buttons to communicate the current state to assistive technologies.
Pausing multiple animations simultaneously
When working with complex interfaces containing several concurrent animations, pausing them individually becomes unwieldy. A more efficient approach is to use a container-based strategy where all child animations react to a single parent state. This pattern is ideal for dashboard widgets, game interfaces, or animated infographics with multiple moving parts.
/* Multi-animation dashboard with unified control */
.animation-dashboard {
--panel-bg: oklch(20% 0.03 260);
--panel-border: oklch(28% 0.04 260);
--accent: oklch(65% 0.22 260);
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 1rem;
padding: 1.5rem;
background: oklch(15% 0.02 260);
border-radius: 1rem;
border: 1px solid var(--panel-border);
}
.dashboard-panel {
background: var(--panel-bg);
padding: 1rem;
border-radius: 0.5rem;
border: 1px solid var(--panel-border);
position: relative;
}
/* Each panel has its own independent animation */
.dashboard-panel .icon {
width: 2rem;
height: 2rem;
background: var(--accent);
border-radius: 50%;
margin-bottom: 0.75rem;
animation: pulse 2s ease-in-out infinite;
}
.dashboard-panel .progress {
height: 4px;
background: oklch(25% 0.04 260);
border-radius: 2px;
overflow: hidden;
}
.dashboard-panel .progress-fill {
height: 100%;
width: 60%;
background: var(--accent);
border-radius: 2px;
animation: progress-sweep 1.5s ease-in-out infinite;
}
.dashboard-panel .status {
font-size: 0.75rem;
color: oklch(75% 0.02 260);
margin-top: 0.5rem;
opacity: 0.7;
animation: fade 1s ease-in-out infinite;
}
/* Pausing all panel animations at once */
.animation-dashboard.is-paused .icon,
.animation-dashboard.is-paused .progress-fill,
.animation-dashboard.is-paused .status {
animation-play-state: paused;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 1;
}
50% {
transform: scale(0.9);
opacity: 0.7;
}
}
@keyframes progress-sweep {
0%, 100% {
transform: translateX(-20%);
}
50% {
transform: translateX(80%);
}
}
@keyframes fade {
0%, 100% {
opacity: 0.5;
}
50% {
opacity: 1;
}
}
/* Control toggle */
.animation-dashboard-controls {
display: flex;
justify-content: center;
gap: 1rem;
margin-top: 1rem;
padding-top: 1rem;
border-top: 1px solid oklch(28% 0.04 260);
}
.animation-dashboard-controls button {
padding: 0.5rem 1rem;
background: oklch(25% 0.04 260);
color: oklch(90% 0.02 260);
border: 1px solid oklch(35% 0.05 260);
border-radius: 0.375rem;
cursor: pointer;
font-size: 0.875rem;
}
The key is applying the is-paused class to the container and using descendant selectors to target all animated descendants. This scales efficiently regardless of how many animations exist within the container. When you add new animated elements later, they automatically inherit the pause behavior.
For even more granular control, you can combine container-level and element-specific states. Group related animations under sub-containers and control each group independently, while still maintaining an overall master pause capability. This layered approach supports complex scenarios like pausing all non-essential animations while allowing critical indicators to continue cycling.
Combining play-state with animation-delay
Pairing animation-play-state with animation-delay enables deferred animation starts and delayed activation patterns. This technique is useful for staged reveals, loading sequences, and progressive disclosure patterns where content becomes visible or animated only after certain conditions are met.
/* Staged reveal with delayed starts */
.staged-reveal {
--reveal-color: oklch(65% 0.22 145);
display: flex;
flex-direction: column;
gap: 0.75rem;
}
.reveal-step {
padding: 1rem;
background: oklch(20% 0.03 260);
border: 1px solid oklch(28% 0.04 260);
border-radius: 0.5rem;
opacity: 0;
transform: translateY(10px);
animation:
slideIn 0.4s ease forwards,
slidePulse 2s ease-in-out infinite;
animation-play-state: paused;
animation-delay: calc(var(--step) * 0.3s), calc(var(--step) * 0.3s);
}
/* Start the first step immediately when activated */
.staged-reveal.is-active .reveal-step:nth-child(1) {
animation-play-state: running;
}
/* Activate subsequent steps progressively */
.staged-reveal.is-active .reveal-step:nth-child(2) {
animation-play-state: running;
animation-delay: 1s, calc(1s + 1s);
}
.staged-reveal.is-active .reveal-step:nth-child(3) {
animation-play-state: running;
animation-delay: 2s, calc(2s + 1s);
}
@keyframes slideIn {
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes slidePulse {
0%, 100% {
border-color: oklch(28% 0.04 260);
}
50% {
border-color: var(--reveal-color);
}
}
/* Button to trigger reveal */
.reveal-trigger {
margin-top: 1rem;
padding: 0.625rem 1.25rem;
background: var(--reveal-color);
color: oklch(15% 0.02 145);
border: none;
border-radius: 0.375rem;
font-weight: 600;
cursor: pointer;
transition: transform 0.15s ease, box-shadow 0.15s ease;
}
.reveal-trigger:hover {
transform: translateY(-1px);
box-shadow: 0 4px 12px oklch(65% 0.22 145 / 40%);
}
.reveal-trigger:active {
transform: translateY(0);
}
Multiple animations can run simultaneously on the same element by separating them with commas. Each animation gets its own timing values for delay and play-state. In this example, the slideIn animation runs once to reveal the element, while slidePulse runs continuously afterward. The animation-delay property accepts comma-separated values corresponding to each animation in the list.
This pattern creates sophisticated timing chains without JavaScript. Each step waits its turn before becoming active, while already-visible elements continue their subtle pulsing. The visual hierarchy reinforces the staged progression, and users can easily track which element just appeared versus which have already been revealed.
Best practices
Follow these guidelines when implementing pause controls:
- Always provide a visible, keyboard-accessible pause button for auto-playing content.
- Use
animation-play-stateinstead of removing the animation entirely. - Respect
prefers-reduced-motion: reduceby defaulting animations to paused or removing them. - Ensure the pause button is focusable and works with keyboard and screen readers.
- Use container-based state classes for controlling multiple animations simultaneously.
- Combine
animation-delaywith play-state for staged or deferred animation sequences. - Test animations with various user preferences enabled, including reduced motion modes.
Pausing animations when the tab is hidden
Infinite animations that run in background tabs waste battery and CPU. The Page Visibility API lets you detect when a tab becomes hidden and pause animations accordingly. Listening for the visibilitychange event gives you a hook to toggle a class that sets animation-play-state: paused on all animated elements.
The most efficient approach applies paused state at the root level. Adding a class to document.documentElement means a single CSS rule can pause every animation on the page simultaneously. When the tab becomes visible again, removing the class resumes all animations exactly where they left off — no resets, no janky restarts. This pattern costs almost nothing and is a genuine quality-of-life improvement for users with many tabs open.
/* Pause all animations when tab is hidden */
.tab-hidden * {
animation-play-state: paused !important;
}
/* Or scope to animated elements specifically */
.tab-hidden .animated {
animation-play-state: paused;
}
Combine this with prefers-reduced-motion handling for a complete accessibility and performance story. Users who request reduced motion get no animations at all; users on hidden tabs get paused animations; everyone else gets the full experience. Three lines of JavaScript and two CSS rules cover the entire matrix of cases.