CSS Animations vs JavaScript Animations
What You Learn From This Page
- How declarative CSS motion differs from imperative JavaScript control.
- When transitions, keyframes, the Web Animations API, or a frame loop fit best.
- Why JavaScript-managed state with CSS-managed motion is a strong UI pattern.
- How gestures, runtime geometry, data, physics, and timelines affect the choice.
- Why property choice and main-thread workload matter more than language labels.
- How to respect reduced-motion preferences and progressive enhancement.
- How to avoid layout thrashing, fragile timing chains, and unnecessary libraries.
- How to select and verify an animation architecture for real components.
Introduction
Web animation can be created with CSS transitions and keyframes, with JavaScript APIs, or with a deliberate combination of both. The useful question is not which technology wins in every situation. It is which layer should own the state, timing, interpolation, and application behavior for a particular interaction.
CSS is usually the clearest choice when motion connects known visual states. A button moves slightly on hover, a menu fades after an open class appears, or a spinner rotates through a predetermined cycle. JavaScript becomes valuable when the endpoint is calculated at runtime, input must be sampled continuously, multiple systems must be coordinated, or code must pause, reverse, seek, and sequence motion.
A practical rule is simple: use CSS for predefined visual interpolation and JavaScript for behavior and dynamic control. Many production interfaces use both. JavaScript decides that a dialog is open, updates the DOM state and manages focus; CSS describes how the dialog enters. This division remains understandable, testable, responsive, and accessible as an application grows.
The Fundamental Difference
CSS animation is declarative. A stylesheet describes states, keyframes, durations, and easing, while the browser determines how to interpolate between values. JavaScript is imperative. Code can decide what happens, when it happens, which values are calculated, and what should follow completion.
This distinction is more important than syntax. Declarative motion lets the rendering engine own scheduling and integration with selectors and media queries. Imperative control can observe application data and alter the sequence while it runs. Neither model is universally faster or more capable. Each moves responsibility to a different part of the system.
CSS Transitions for State Changes
A transition is ideal when an element moves between a small number of meaningful states. The browser watches a transitionable property, detects its changed value, and interpolates from the old value to the new one.
.button {
transform: translateY(0);
transition: transform 180ms ease-out;
}
.button:hover,
.button:focus-visible {
transform: translateY(-3px);
}
The interaction already exists in CSS through :hover and :focus-visible, so a JavaScript event handler would add code without adding useful behavior. Transitions also work naturally when JavaScript changes a class or attribute. CSS still owns the visual definition, even though application logic initiated the state change.
CSS Keyframes for Predetermined Motion
Keyframes fit motion with a known path or repeating cycle. A loading indicator, attention pulse, skeleton shimmer, or decorative entrance can be described from start to finish without calculating every frame.
@keyframes spin {
to {
transform: rotate(1turn);
}
}
.spinner {
animation: spin 800ms linear infinite;
}
The browser can start the effect as soon as the selector matches. There is no initialization routine, element lookup, timer, or cleanup callback. That simplicity is valuable, but it should not justify continuous decoration with no user benefit. Infinite animation still consumes rendering resources and power.
JavaScript Animation Fundamentals
JavaScript can modify classes, custom properties, inline styles, SVG attributes, canvas pixels, or animation objects. Its advantage is access to runtime information and application decisions. An effect can depend on a score, API response, pointer velocity, measured container size, or a promise resolving.
Directly changing styles does not automatically create smooth motion. The implementation must use an animation-capable API or update values in coordination with rendering. It must also avoid expensive per-frame work. JavaScript supplies control; it does not remove responsibility for the rendering pipeline.
The Web Animations API
The Web Animations API exposes browser-managed animation to JavaScript through Element.animate(). It combines keyframe-style interpolation with programmatic playback control.
const animation = panel.animate(
[
{ opacity: 0, transform: "translateY(20px)" },
{ opacity: 1, transform: "translateY(0)" }
],
{
duration: 400,
easing: "ease-out",
fill: "forwards"
}
);
The returned animation object can be paused, resumed, reversed, canceled, inspected, or assigned a different playback rate. The browser performs interpolation, so the API should not be confused with manually writing every frame. It is a strong fit when keyframes are appropriate but their timing and lifecycle depend on runtime logic.
Playback and Completion Control
Programmatic control is useful when the interface must respond while motion is active. A panel can reverse immediately when the user changes direction rather than finishing one transition and starting another. A tutorial can pause an illustration, or a timeline can accelerate during a skip action.
animation.pause();
animation.playbackRate = 1.5;
animation.reverse();
await animation.finished;
updateApplicationState();
The finished promise makes sequencing explicit, but production code must also consider cancellation and replacement. An animation canceled before completion can reject its promise. Lifecycle control should be centralized instead of spread across unrelated event handlers.
JavaScript State and CSS Motion
The most common production pattern is a hybrid: JavaScript owns behavior and CSS owns visual interpolation. A click handler updates a class, data attribute, or native state. CSS reacts to that state.
button.addEventListener("click", () => {
panel.classList.toggle("open");
});
.panel {
opacity: 0;
transform: translateY(-0.5rem);
transition:
opacity 150ms ease,
transform 180ms ease-out;
}
.panel.open {
opacity: 1;
transform: translateY(0);
}
This structure avoids scattering design values through scripts. Designers can tune timing and easing in the stylesheet, while developers can change state logic without rebuilding the motion engine. It also allows media queries and user preferences to modify the animation independently.
Classes, Attributes, and Native State
Classes are convenient, but they are not the only bridge. Attributes such as aria-expanded, data-state, open, checked, and disabled can expose meaningful state to selectors. Using existing semantic state reduces duplication between accessibility logic and presentation.
.disclosure[aria-expanded="true"] + .details {
opacity: 1;
transform: translateY(0);
}
JavaScript must still implement actual behavior. A modal needs focus management, Escape handling, labeling, and background interaction control. CSS can animate its surface, but it cannot replace the interaction contract.
Hover, Focus, Active, and Checked States
Pseudo-classes make CSS especially effective for local interaction feedback. Hover, keyboard focus, active presses, checked controls, and disabled states already belong to the selector model. A small transform, color change, or opacity transition can follow those states directly.
Do not implement ordinary hover with mouseenter and mouseleave handlers unless the behavior depends on data or another system. Extra handlers increase state synchronization work and can overlook keyboard or touch users. CSS selectors express these simple relationships more completely.
Responsive and Environmental Motion
CSS integrates with media queries, container queries, and user preferences. A motion treatment can change at a breakpoint, disappear for reduced motion, or adapt to interaction capabilities without a script managing listeners.
@media (prefers-reduced-motion: reduce) {
.animated {
animation: none;
transition-duration: 0.01ms;
}
}
JavaScript can query the same preference with matchMedia() when programmatic animation is involved. If the preference may change while the page is open, code should observe changes rather than reading the value only once.
Complex Sequencing
CSS delays can create a short predetermined stagger, but they become hard to reason about when a sequence branches, waits for data, reacts to cancellation, or coordinates several components. A chain of unrelated delays also encodes dependencies indirectly: changing one duration can shift every later event.
JavaScript timelines or Web Animations promises can express the sequence directly. Code can await one stage, test current state, start parallel work, and clean up if the user leaves. For a fixed decorative entrance, CSS remains simpler; for application choreography with logic, explicit orchestration is safer.
Runtime Geometry and Dynamic Endpoints
An endpoint is sometimes unknown until layout is measured. A shared element may travel from one card to another, a tooltip may avoid the viewport edge, or a drawer may move by its rendered width. JavaScript can use getBoundingClientRect() and other geometry APIs to calculate those values.
Measurements should not be mixed carelessly with style writes. Reading layout after changing styles can force the browser to synchronously calculate geometry. Gather required reads together, calculate in memory, and then perform writes together. Resize and content changes also need a clear invalidation strategy.
Data-Driven Animation
Progress meters, charts, upload indicators, and live dashboards derive visuals from application data. JavaScript must interpret the data, but it does not need to own all presentation. A custom property forms a clean bridge.
const ratio = completed / total;
meter.style.setProperty("--progress", ratio);
.meter-fill {
transform: scaleX(var(--progress));
transform-origin: left;
transition: transform 250ms ease-out;
}
This pattern gives JavaScript responsibility for the number and CSS responsibility for appearance. The value should also be exposed textually and with appropriate semantics, because animation alone cannot communicate progress accessibly.
Gestures and Direct Manipulation
Dragging, swiping, pinching, and scrubbing require continuous pointer or touch information. JavaScript tracks input, applies constraints, determines velocity, handles cancellation, and updates state. CSS can style hover or dragging states, but selectors cannot implement the complete gesture.
Direct manipulation must remain connected to input. Expensive work inside pointer handlers creates lag that users immediately feel. Capture necessary coordinates, schedule visual updates through one rendering callback, and avoid creating a new animation object for every raw pointer event.
Physics and Game-Like Motion
Springs, momentum, friction, gravity, collision, and drag inertia evolve according to values from the previous frame. Their destination and duration may not be known in advance. JavaScript or a focused animation library is a natural fit because it can integrate velocity and position over elapsed time.
CSS keyframes can imitate a particular spring curve, but they do not model a spring responding to a changing target. Game loops, simulations, and particle systems often belong in Canvas or WebGL rather than hundreds of animated DOM elements. The rendering medium should match the amount and type of visual state.
requestAnimationFrame for Manual Loops
When JavaScript must calculate each frame, use requestAnimationFrame(). The browser invokes the callback before a repaint, can align work with the display, and may throttle updates when a document is not visible.
let previousTime;
function update(time) {
const delta = previousTime ? time - previousTime : 0;
previousTime = time;
advanceSimulation(delta);
renderCurrentState();
requestAnimationFrame(update);
}
requestAnimationFrame(update);
Use the callback timestamp rather than assuming every frame lasts 16 milliseconds. Displays can refresh at different rates, frames can be delayed, and background tabs behave differently. Time-based motion remains consistent when frame timing varies.
Why Timers Are Not Animation Engines
setInterval() and repeated setTimeout() calls are not synchronized with rendering. Their callbacks may run late, compete with other tasks, or produce updates the browser cannot display. An interval of 16 milliseconds does not guarantee 60 smooth frames per second.
Timers remain useful for application scheduling, but frame-driven visual updates belong in requestAnimationFrame(). Better still, use CSS or the Web Animations API when no custom per-frame calculation is needed, because those approaches avoid running application code on every frame.
CSS Animation Events
CSS and JavaScript can cooperate through animationstart, animationiteration, animationend, transitionrun, transitionstart, transitionend, and transitioncancel. These events help when application behavior truly depends on the visual lifecycle.
panel.addEventListener("transitionend", event => {
if (event.propertyName === "opacity") {
finalizePanelState();
}
});
Events can fire once per transitioned property, so filters matter. They also should not become the only path to essential functionality. Reduced-motion settings, interrupted transitions, detached nodes, or changed styles can alter whether and when completion occurs.
Performance Is Not CSS Versus JavaScript
The claim that CSS always uses the GPU while JavaScript always uses the CPU is false. Both approaches ultimately ask the browser to update visual properties. A Web Animations transform can be compositor-friendly, while a CSS keyframe that changes width can repeatedly trigger layout.
Performance depends on the changed properties, affected area, layer and paint behavior, script workload, and device. Transform and opacity are generally strong choices for frequent visual motion because they can often avoid layout and paint. They are optimization opportunities, not unconditional guarantees.
Layout, Paint, and Composition Costs
Animating width, height, margins, offsets, or other geometry can trigger layout. Shadows, gradients, filters, backgrounds, and text effects may require paint. Transform and opacity may be handled during composition after content has been rasterized.
The same rendering stages apply whether values came from a stylesheet or script. Before rewriting CSS as JavaScript, identify the costly stage in browser DevTools. Changing the control mechanism does not repair an expensive property or an oversized surface.
Main-Thread JavaScript and Frame Budgets
JavaScript animation can stutter when the main thread is busy with large calculations, rendering framework work, style recalculation, or garbage collection. Per-frame callbacks must be short and predictable. A long unrelated task can also delay input and animation even when the animation code itself is small.
Browser-managed compositor animation may continue more independently in some conditions, which is one reason to avoid unnecessary frame callbacks. However, CSS is not free: the browser still schedules animation, manages layers, composites pixels, and presents frames. Measure the whole workload.
Avoiding Layout Thrashing
Layout thrashing occurs when code alternates geometry reads and style writes in a way that repeatedly forces layout. A loop that reads an element width, changes a position, then reads another width can prevent the browser from batching work.
// Read first.
const bounds = items.map(item => item.getBoundingClientRect());
// Calculate in memory, then write.
items.forEach((item, index) => {
item.style.transform = `translateX(${bounds[index].left}px)`;
});
Frameworks and helper libraries do not automatically prevent this problem. Use performance traces to find forced reflow warnings and inspect the call stack that caused them.
Progressive Enhancement
Animation should enhance a usable default state. If content starts hidden and JavaScript must reveal it through an entrance effect, a script error can leave essential information invisible. Keep content visible by default, and apply hidden initial states only after the animation system is known to be active.
This principle also supports older browsers, slower devices, print output, and automation. The page should remain understandable if animation APIs are unavailable, style sheets load late, or users suppress motion.
Reduced Motion in CSS and JavaScript
Motion can trigger dizziness, nausea, distraction, or loss of focus. Both CSS and JavaScript animation must honor prefers-reduced-motion. Reduced motion means reducing unnecessary movement, not removing functionality or hiding state changes.
const motionQuery = window.matchMedia(
"(prefers-reduced-motion: reduce)"
);
if (!motionQuery.matches) {
runProgrammaticAnimation();
}
Large travel, zooming, parallax, and continuous movement are common candidates for removal. An instant update or subtle fade can preserve orientation. Stop existing JavaScript animations if the preference changes while the page is open.
Accessible Interaction Remains Separate
Animation does not create accessible behavior. A dropdown still needs correct focus order and keyboard handling. A dialog still needs a name, modal semantics, focus containment, restoration, and an operable close control. A carousel needs understandable controls and should not move unexpectedly.
Visual order should agree with reading and focus order. Motion should not be the only indication that state changed. Announce important asynchronous outcomes through appropriate text and semantics rather than expecting users to notice an animated element.
Component Example: Modal Dialog
JavaScript should open and close the dialog, manage focus, respond to Escape, and restore focus to the trigger. CSS can animate opacity and a small transform. This separation prevents presentation code from becoming responsible for safety-critical interaction behavior.
When closing, teams sometimes wait for a transition before setting the element to hidden. That workflow must account for reduced motion and interruption. A short timeout fallback or a centralized animation utility can prevent the dialog from remaining interactive after the visual exit should have finished.
Component Example: Accordion
An accordion button updates aria-expanded and controls the associated region. CSS can animate an inner wrapper using a grid-row technique or a measured height. The grid technique avoids hard-coded content height for many layouts.
.accordion-content {
display: grid;
grid-template-rows: 0fr;
transition: grid-template-rows 300ms ease-in-out;
}
.accordion[aria-expanded="true"] .accordion-content {
grid-template-rows: 1fr;
}
.accordion-content > div {
overflow: hidden;
}
JavaScript owns expansion state and keyboard behavior; CSS owns the visual reveal. If content or browser support makes the CSS technique unsuitable, JavaScript may measure height, but that complexity should be justified.
Component Example: Carousel
A carousel illustrates the boundary between state and motion. JavaScript tracks the active slide, labels controls, pauses autoplay, and responds to navigation. CSS can translate the track and transition between known positions.
Gesture velocity, infinite looping, cloned slides, dynamic item widths, and snapping introduce more programmatic control. Accessibility also becomes harder as complexity rises. Animation technology is only one part of the decision; the component must remain operable and understandable.
Component Example: Tooltip
A simple explanatory tooltip may use CSS for focus and hover visibility. A robust tooltip often needs JavaScript to calculate placement, detect collisions, update after scrolling, dismiss on Escape, and maintain semantics.
The visual fade can still remain in CSS. Keeping placement logic separate from styling means the tooltip can change direction without embedding every color, duration, and easing value in the positioning script.
Component Example: Progress and Loading
Use CSS keyframes for a spinner whose rotation is predetermined. JavaScript should decide whether the loading state exists, not calculate every rotation. For determinate progress, JavaScript supplies the current value and CSS displays it through transform, width, or a native progress element.
A loading indicator should not be the only feedback. Provide meaningful status text, avoid announcing every tiny percentage change, and stop continuous animation after loading completes or the element leaves the interface.
Scroll-Driven Animation
Scroll progress, element visibility, and direction can drive motion. Modern CSS includes scroll-driven animation capabilities, while JavaScript can use observers and frame callbacks for more complex behavior. The choice depends on browser support and required logic.
Avoid handling every scroll event with synchronous measurements and style changes. Intersection Observer is suitable for visibility thresholds, and CSS may handle straightforward scroll timelines. JavaScript remains appropriate for coordinated data loading, application state, or calculations that CSS cannot express.
Animation Libraries
Libraries can provide timelines, spring solvers, gesture recognition, SVG morphing, route transitions, and framework lifecycle integration. These features may greatly reduce bespoke code for a complex motion system.
The tradeoff includes bundle size, initialization cost, API knowledge, dependency updates, and another abstraction to debug. Installing a library for a button hover is not justified. Choose one when its control model solves recurring requirements better than native CSS and browser APIs, then measure its loading and runtime cost.
Maintainable Ownership of Motion
Motion values scattered across CSS, inline styles, data attributes, component props, and library configuration become difficult to tune. Define ownership. Keep reusable durations, easing curves, and distances in design tokens. Keep application decisions in state logic. Keep component presentation near its styling system.
JavaScript custom properties are useful boundaries because code provides dynamic values without taking over the entire declaration. Class and attribute changes are preferable when a finite semantic state already exists. Direct inline mutation is appropriate for genuinely continuous values such as a drag position.
Testing Animation Behavior
Test final state and interaction behavior rather than relying only on exact elapsed time. Automated tests should verify that the correct class, attribute, or animation object is produced and that cancellation leaves a valid state. Tests should run with reduced motion where possible to reveal hidden dependencies on completion events.
Manual testing should include keyboard use, zoom, mobile devices, high-refresh displays, slow hardware, tab switching, rapid repeated input, and interrupted navigation. Use browser Performance and Animations panels to inspect long tasks, dropped frames, layout, paint, and compositor activity.
Practical Decision Framework
Start with the simplest model that represents the interaction. If motion connects known CSS states, use a transition. If it follows predetermined keyframes, use CSS animation. If keyframes need runtime playback control, consider the Web Animations API. If every frame depends on simulation or direct input, use requestAnimationFrame() or a suitable library.
Then evaluate properties, accessibility, interruption, and failure. Decide who owns state and cleanup. Confirm that motion still works when content changes, preferences reduce motion, and the user acts faster than the nominal duration. Finally, profile on realistic hardware instead of assuming the chosen technology guarantees performance.
CSS and JavaScript Animation Comparison
| Requirement | CSS | JavaScript |
|---|---|---|
| Hover, focus, and active feedback | Excellent | Usually unnecessary |
| Simple open and closed states | Excellent for motion | Useful for behavior and state |
| Predetermined keyframe loop | Excellent | Possible but often unnecessary |
| Dynamic endpoint | Limited | Excellent |
| Gesture tracking | Limited | Excellent |
| Physics and simulation | Poor fit | Excellent |
| Complex conditional sequence | Awkward | Excellent |
| Pause, reverse, seek, and rate control | Limited | Excellent with WAAPI |
| Media-query integration | Excellent | Requires matchMedia logic |
| Data-driven values | Receives custom properties well | Excellent source of values |
| Simple implementation complexity | Usually lower | Usually higher |
| Rendering performance | Depends on properties and workload | Depends on properties and workload |
Common Mistakes and Better Choices
| Mistake | Better Choice |
|---|---|
| Using JavaScript for every hover and focus effect | Use pseudo-classes and CSS transitions |
| Forcing gesture physics into CSS keyframes | Use a JavaScript simulation or focused library |
Animating left every frame for visual movement | Prefer a measured transform where appropriate |
Using setInterval() as a frame scheduler | Use requestAnimationFrame() |
| Assuming CSS is always faster | Inspect changed properties and rendering stages |
| Assuming JavaScript is always slow | Use WAAPI or focused frame work with efficient properties |
| Waiting on visual completion for essential behavior | Handle cancellation, reduced motion, and fallback completion |
| Hiding content until JavaScript animates it | Use progressive enhancement and a visible base state |
| Ignoring repeated layout reads and writes | Batch measurements and mutations |
| Adding a large library for one small effect | Use native CSS or Web Animations |
| Scattering motion values across systems | Define clear ownership and shared tokens |
| Ignoring reduced motion in script | Observe the preference and stop unnecessary motion |
Common Interview Questions
What is the main difference between CSS and JavaScript animations? CSS declaratively interpolates predefined visual states, while JavaScript provides runtime decisions, calculations, orchestration, and playback control.
Are CSS animations always faster? No. Performance depends more on animated properties, rendering work, affected area, script load, and device than on whether the values originate in CSS or JavaScript.
When should CSS be preferred? Prefer CSS for hover, focus, active feedback, simple state transitions, predetermined keyframes, and motion that should adapt directly through media queries.
When should JavaScript be preferred? Use JavaScript for dynamic endpoints, gestures, physics, runtime sequencing, data-driven values, callbacks, and programmatic playback control.
What is a common hybrid architecture? JavaScript updates semantic state through a class or attribute, and CSS transitions the visual properties associated with that state.
What API provides browser-managed animation control in JavaScript? The Web Animations API, commonly accessed through Element.animate().
What should schedule manually calculated frames? Use requestAnimationFrame() and calculate movement from elapsed time rather than an assumed frame rate.
How should reduced motion be handled? Use the CSS media feature for stylesheet motion and matchMedia() for programmatic motion, while preserving all functionality.
Practical Review Checklist
- Use CSS transitions for simple, known state changes.
- Use CSS keyframes for predetermined sequences and loops.
- Use JavaScript for runtime data, gestures, geometry, physics, and branching.
- Consider the Web Animations API when keyframes need playback control.
- Use
requestAnimationFrame()for genuine per-frame calculations. - Let JavaScript own behavior while CSS owns presentation where practical.
- Prefer semantic attributes or classes over scattered inline style mutation.
- Use custom properties to pass dynamic values into CSS.
- Prefer transform and opacity for frequent visual movement where appropriate.
- Do not assume the technology label determines rendering performance.
- Batch layout reads and style writes to avoid forced synchronous layout.
- Keep frame callbacks short and remove them when no longer needed.
- Respect reduced motion in both CSS and JavaScript.
- Keep content and controls usable if animation fails or is disabled.
- Handle interruption, cancellation, rapid input, and completion cleanup.
- Test keyboard interaction, focus, semantics, zoom, and screen sizes.
- Profile layout, paint, compositing, long tasks, and dropped frames.
- Add an animation library only when repeated complex requirements justify it.
Key Takeaway
CSS and JavaScript animations solve different parts of interface motion. CSS is concise and expressive when the browser can interpolate between known visual states. JavaScript is the right control layer when animation depends on application logic, measured geometry, live data, direct input, physics, or a sequence that changes while running.
For many components, the strongest architecture combines them: the user acts, JavaScript updates meaningful state, and CSS animates the resulting visual change. When more control is needed, the Web Animations API provides programmable browser-managed keyframes; when every frame must be calculated, requestAnimationFrame() provides the proper scheduler. Whichever approach you choose, property cost, main-thread work, interruption, reduced motion, and accessible behavior determine the real quality of the result.