How to Create Responsive Animations
Build animations that adapt to viewport and component size, respect reduced-motion preferences, and avoid unnecessary rendering work.
Use a viewport media query when motion should change with the viewport or device; use a container query when a reusable component should respond to the space its own container provides. In either case, account for prefers-reduced-motion, keep essential information available without animation, and prefer transform or opacity when they suit the effect.
1. Choose the responsive condition
First decide what should trigger the change:
- Viewport or device condition: use a media query. For example, a page-wide intro effect may be appropriate on a wide viewport but distracting on a narrow one.
- Component width: use a container query when the same component may appear in differently sized columns, sidebars, or cards. Its animation can then respond to its available space rather than the overall screen.
- User motion preference: use the
prefers-reduced-motionmedia feature to offer a static or reduced-motion presentation.
Media queries apply conditional styles based on viewport values and browser or device features. Container queries let styles respond to a containing element’s size, which is useful for reusable components. MDN: Using media queries and MDN: CSS container queries.
2. Build a simple responsive animation
This complete example animates a decorative accent when a card appears. The card’s layout and content work without motion. The animation changes based on the card’s container width, while a viewport query turns off the accent on a narrow screen. Users who request reduced motion get the static state.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Responsive animation example</title>
<style>
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
display: grid;
place-items: center;
padding: 1.5rem;
font: 1rem/1.5 system-ui, sans-serif;
background: #f4f6fa;
color: #172033;
}
.card-wrap {
width: min(100%, 42rem);
container-type: inline-size;
}
.card {
position: relative;
overflow: hidden;
padding: clamp(1.25rem, 5cqi, 2.5rem);
border-radius: 1rem;
background: white;
box-shadow: 0 1rem 3rem rgb(22 34 58 / 12%);
isolation: isolate;
}
.card h1 { max-width: 18ch; margin: 0 0 0.5rem; }
.card p { max-width: 55ch; margin: 0; }
.accent {
position: absolute;
z-index: -1;
top: 1rem;
right: 1rem;
width: 3rem;
aspect-ratio: 1;
border-radius: 50%;
background: #8b7cff;
opacity: 0.22;
transform: scale(0.75);
animation: arrive 700ms cubic-bezier(.2,.8,.2,1) both;
}
@keyframes arrive {
from { opacity: 0; transform: translateY(-1rem) scale(0.5); }
to { opacity: 0.22; transform: translateY(0) scale(1); }
}
/* The component is compact: keep its decoration still. */
@container (max-width: 24rem) {
.accent { animation: none; transform: scale(0.75); }
}
/* This page-wide condition is about the viewport. */
@media (max-width: 30rem) {
.accent { display: none; }
}
/* Static styles are the safe baseline for reduced motion. */
@media (prefers-reduced-motion: reduce) {
.accent { animation: none; transform: scale(0.75); }
}
</style>
</head>
<body>
<main class="card-wrap">
<article class="card">
<span class="accent" aria-hidden="true"></span>
<h1>Motion that fits its context</h1>
<p>The card remains readable and complete whether its accent animates or stays still.</p>
</article>
</main>
</body>
</html>
Save this as an HTML file and open it in a browser. Resize the viewport and the card’s containing area to see the two different conditions. The exact breakpoints are design choices: use the widths where the layout or motion stops making sense, rather than treating these example values as universal.
3. Respect reduced-motion preferences
The prefers-reduced-motion feature can match reduce or no-preference. A reduced-motion preference means the user prefers movement to be minimized. For nonessential effects, turn off the animation or provide a less dynamic state while keeping the content and functionality intact. MDN: prefers-reduced-motion.
An alternative is to make the static presentation the default and enable motion only for users who indicate no preference:
.notice { opacity: 1; transform: none; }
@media (prefers-reduced-motion: no-preference) {
.notice {
animation: enter 400ms ease-out both;
}
}
@keyframes enter {
from { opacity: 0; transform: translateY(0.5rem); }
to { opacity: 1; transform: translateY(0); }
}
Choose the pattern that makes the baseline easiest to reason about. Do not hide information, make controls harder to use, or remove functionality when reducing motion. Some animation is essential to understanding or operating a feature; preserve the necessary result and consider a simpler presentation.
WCAG 2.2 Success Criterion 2.3.3 concerns nonessential animation triggered by interaction. Automatically initiated animation is addressed under a different criterion. The W3C describes its C39 media-query technique as one example, not a required method for conformance. See W3C WAI: Understanding Animation from Interactions and W3C WAI Technique C39.
4. Choose CSS or JavaScript based on control needs
CSS is a good fit for declarative effects such as a component entering, a hover state, or a transition between known visual states. Use JavaScript when the animation needs programmatic sequencing, playback control, or coordination with application state. The Web Animations API provides a JavaScript interface to animations. Neither approach is automatically right for every effect; keep the motion tied to the UI state and provide a reduced-motion path.
For example, JavaScript can apply a simple animation only when motion is allowed:
const motionQuery = matchMedia('(prefers-reduced-motion: reduce)');
const panel = document.querySelector('.panel');
function showPanel() {
if (!panel) return;
if (motionQuery.matches) {
panel.hidden = false;
return;
}
panel.hidden = false;
panel.animate(
[
{ opacity: 0, transform: 'translateY(8px)' },
{ opacity: 1, transform: 'translateY(0)' }
],
{ duration: 240, easing: 'ease-out' }
);
}
motionQuery.addEventListener('change', () => {
// Apply the current preference to any in-progress or future UI behavior.
});
This snippet assumes the page contains an element with class panel. In a real interface, decide what should happen to an animation already in progress if the preference changes, and keep the final state correct if the animation is skipped or canceled.
5. Keep rendering work under control
Prefer animating transform and opacity when they can express the intended effect. Changes to dimensions, padding, borders, margins, and positional layout properties can trigger layout work and repainting. Effects such as filters can also have costs that depend on the page and device. Avoid animating large areas or many elements without a reason, and remove decorative movement that does not help the user.
Do not add will-change preemptively. MDN recommends it as a last resort when an existing performance problem has been identified. Profile the real page and target devices before adding rendering hints. See MDN: CSS and JavaScript animation performance.
6. Check behavior across sizes and preferences
- Test the layout at the narrow, intermediate, and wide sizes your design supports. Check both the viewport and components placed in different container widths.
- Enable reduced motion in the operating system or browser, then reload and interact with the page. Confirm that essential content, state changes, and controls remain clear.
- Test interaction-triggered effects separately from animations that start automatically. Confirm users can still understand and operate the page if nonessential movement is absent.
- Inspect performance on representative devices, especially when many elements animate or the effect covers a large area.
- Check the browser matrix your project supports. Container queries, media features, and animation APIs should be verified against that target matrix; do not assume support for a newer feature without checking.
7. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The component query never matches | The queried component has no container context, or the queried width is not the width you expect. | Set an appropriate container type on the ancestor, query the intended ancestor, and inspect its actual size. Keep a usable base style for browsers outside the target feature set. |
| Reduced motion still animates | The rule is overridden, another animation is applied through script, or the preference was not enabled in the environment being tested. | Check computed styles and animation sources, inspect rule order and specificity, and verify the OS or browser motion setting. |
| Content disappears when animation is disabled | The content was made visible only by an animation’s final keyframe or fill behavior. | Put the correct visible state in the base styles. Treat the animation as enhancement, not as the source of required content visibility. |
| The animation feels sluggish or causes page jumps | It may animate layout-affecting properties, too many elements, or an overly large area. | Reduce the animated area or count and consider a transform or opacity effect. Profile before applying rendering hints. |
| A one-time effect does not replay | The animation completed and its state or class was not reset. | For CSS, remove and reapply the class when the UI state calls for replay. For the Web Animations API, retain the returned animation object and control its playback deliberately. |
| Motion preference changes during use are ignored | The page checked the preference only once and did not respond to changes. | Listen for changes to the media query if the application needs to update active UI behavior, and ensure the nonanimated state remains valid. |
8. Or skip the browser setup
If you need a screenshot of the finished page at responsive sizes, ScreenshotNeo can capture a URL with one API request. See the ScreenshotNeo API documentation.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
import requests
r = requests.get("https://api.screenshotneo.com/v1/shot", params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"}, timeout=90)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
ScreenshotNeo accepts consent banners like a visitor and removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture; each cleanup step can be turned off. Bot checks, blank pages, failed loads, timeouts, and cache hits are not billed, and response headers report the page verdict and billing status. Its MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. Every feature is available on every plan. Visit ScreenshotNeo or sign up free for 1,000 screenshots a month, with no card required.
9. FAQ
Should every animation be removed for reduced-motion users?
No. Minimize nonessential movement and preserve essential information and functionality. A still state or a less dynamic alternative can meet the need without making the interface harder to use.
Do container queries replace media queries?
No. Use a media query for a viewport or device condition and a container query for a component’s available space. A page can use both.
Is CSS always faster than JavaScript for animation?
No universal rule applies to every effect and page. Choose CSS for suitable declarative effects and JavaScript when programmatic control is needed, then assess actual rendering cost.
Does using prefers-reduced-motion alone prove WCAG conformance?
No. It is a useful implementation technique, while conformance depends on the applicable requirements and the full experience. W3C’s C39 is an example technique rather than a mandatory method.


