Responsive Animation: Designing for Every Screen

Animating for the web means accepting that there are no export settings. Unlike video, where you design within a fixed ratio and render it out, web animation must adapt to whatever device, browser, and viewport it lands on. The challenge isn't just making things move—it's making them move correctly across an infinite range of contexts.

Before writing any code, it's worth asking how the animation will be used. Will it be a repeated module across the application? Does it need to scale at all? Answering those questions first determines the right technical approach and prevents wasted effort.

Most animations fall into three categories:

  • Fixed: Icons, loaders, and other elements that keep their size and aspect ratio everywhere. Hard-coded pixel values work fine here.
  • Fluid: Animations that need to scale smoothly across devices. Most layout animations belong here.
  • Targeted: Animations specific to certain screen sizes or input methods—desktop-only effects, touch interactions, or hover states that change substantially at breakpoints.

Fluid and targeted animations each demand their own strategies.

Fluid Animation: Let the Browser Do the Work

As Andy Bell puts it: "Be the browser's mentor, not its micromanager—give the browser some solid rules and hints, then let it make the right decisions for the people that visit it." Fluid animation is largely about choosing the right units from the start. Viewport units scale automatically as the browser resizes, and percentage-based movement continues to work even when elements change width at breakpoints.

When animating, avoid layout properties like left and top, which trigger layout reflows and janky motion. Stick to transforms and opacity wherever possible.

SVG User Units

SVG offers a distinct advantage: user units that are responsive out of the box. All elements are plotted on an infinite coordinate grid, and the viewBox defines the visible portion of that grid. Animating an element by a set number of units moves it by that many units relative to the SVG's own coordinate system—so animating by the full width of the viewBox always traverses the entire visible area, regardless of the SVG's rendered size.

viewBox="0 0 100 50”

This same behavior is harder to achieve with HTML elements. Previously, you'd need JavaScript to grab the parent's width—straightforward when animating from a transforms position, but fiddly when animating to one and resizing changes the endpoint. If you do recalculate on resize, always debounce the handler; resize listeners fire constantly, and updating properties on each event is heavy work for the browser.

Container units solve this problem natively, making animation values relative to parent elements rather than the viewport. Browser support was initially limited to Chrome and Safari but has been expanding—check Caniuse for current details.

After nearly 15 years as a highly-requested (impossible!?) feature, size-based Container Queries & units have shipped in both Chrome/Edge 105 & Safari 16! Firefox is not far behind.

(also: there's a prototype of style queries!)https://t.co/A2zgd9l4FC

— @[email protected] (@TerribleMia) September 15, 2022

FLIP for Layout Transitions

In SVG-land every element sits on one grid, which makes responsive movement easy. HTML is a different story: layouts rely on varied positioning methods and flex or grid systems, and many layout changes can't be animated directly. Moving an element between relative and fixed positioning, shuffling flex children around the viewport, or re-parenting a node in the DOM all break normal transitions.

The FLIP technique handles these cases. Its premise is simple:

  • First: Record the elements' starting positions.
  • Last: Make the layout change and record the final positions.
  • Invert: Calculate the visual delta and apply inverse transforms so elements appear to stay in their original spots—even though they're already in the new layout.
  • Play: Remove the inverted transforms, animating from the faked first state to the real final state.

GSAP's FLIP plugin automates the entire sequence. For the underlying concept, Paul Lewis's original write-up remains the definitive explainer.

Scaling SVG and Canvas

Before animating an SVG, make sure it scales the way you intend. The preserveAspectRatio attribute controls how an SVG fits within its container when aspect ratios differ, much like CSS background-position and background-size combined. The value is composed of an alignment plus a meet or slice reference:

  • slice behaves like background-size: cover—scale to fill and crop the overflow.
  • meet behaves like background-size: contain—scale to fit everything inside.

For example, preserveAspectRatio="MidYMax slice" aligns to the middle x-axis and bottom y-axis, scaling to cover; "MinYMin meet" aligns left and top while keeping the whole viewBox visible. One practical trick: set overflow: visible on the SVG and wrap it in a max-height container, so the graphic crops vertically below a certain browser width while revealing extra "stage left" and "stage right" area on wider screens.

Canvas requires more manual management than SVG, though it performs better for animations with many moving parts. A common pattern is to set up the canvas with a fixed internal aspect ratio and unit system—mirroring SVG's approach—then redraw on resize. Since canvas redraws are expensive, debounce them or wait for the resize to settle. Libraries like George Francis's VBCanvas add viewBox and preserveAspectRatio support directly to canvas, recreating SVG-like behavior.

Targeted Animation: Conditions and Cleanup

Sometimes fluid scaling isn't the goal. Mobile screens have limited space and less performance headroom, so it can make sense to serve reduced animation—or none at all. "Sometimes the best responsive animation for mobile is no animation at all! For mobile UX, prioritize letting the user quickly consume content versus waiting for animations to finish."

Media queries target specific viewport sizes just as they do in CSS. JavaScript animations add complexity: an animation must be created only when its media query matches, and fully torn down—killed, released for garbage collection, and cleared of inline styles—when it no longer does. Silently hiding an animation with opacity: 0 doesn't stop it from consuming resources.

(prefers-reduced-motion) /* find out if the user would prefer less animation */

(orientation: portrait) /* check the user's device orientation */

(max-resolution: 300dpi) /* check the pixel density of the device */

GSAP's gsap.matchMedia() (available since 3.11.0) centralizes this workflow. Animation code goes into a function that executes only when a given media query matches; when the query stops matching, all GSAP animations and ScrollTriggers inside it revert automatically. This also makes it easy to hook into media features beyond screen size—checking prefers-reduced-motion, for example, so users who find animation disorienting get a calmer experience.

Beyond Width: Input and Interaction

Screen width is only one axis of responsiveness. Input capabilities matter just as much. The hover media feature tests whether the primary input can hover, and pointer distinguishes mouse from touch. Basing decisions on width alone makes assumptions that break on devices like iPad Pros or Windows Surfaces, whose pointer type can change based on hardware state. Better to treat input device as the primary factor, supported by width as a secondary consideration.

For GSAP users, ScrollTrigger.isTouch exposes this directly in JavaScript:

  • 0 — no touch (pointer or mouse only)
  • 1 — touch-only device
  • 2 — device supports both touch and mouse or pointer input
if (ScrollTrigger.isTouch) {
  // any touch-capable device...
}

// or get more specific: 
if (ScrollTrigger.isTouch === 1) {
  // touch-only device
}

Responsive scrolling animations face another common pitfall: stale measurements. If a scrubbed animation's progress depends on screen size and the browser resizes mid-scrub, values go stale and the animation breaks. Fix this by putting size-dependent calculations into functional values and setting invalidateOnRefresh: true. ScrollTrigger then recalculates those values on resize.

A Few Extra Considerations

Motion Principles: Distance, Speed, and Quantity

With responsive animation, it's easy to forget that speed and momentum should feel related to distance. Real-world objects take longer to travel farther; animation that mimics that reads as believable. An element crossing a large desktop viewport should move differently from the same effect on a phone. Sometimes that means adjusting duration dynamically based on screen width—clamping a value from window.innerWidth and mapping it to a duration with utilities like gsap.utils.

Spacing and quantity also need attention across breakpoints. Environmental decorations—parallax layers, clouds, confetti—should scale and change count with the viewport. Large screens warrant more elements spread throughout; small screens need only a few to create the same sensation. Treat the viewport like a stage: adding and removing content can be part of the choreography. "When designing responsive animations, the challenge is not how to cram the same content into the viewport so that it 'fits,' but rather how to curate the set of existing content so it communicates the same intention." Elements entering or exiting the frame deserve the same visual and thematic care as their movement within it.

The Mobile Resize Trap

On mobile, the browser address bar's show/hide behavior fires resize events. Each one triggers a ScrollTrigger.refresh(), which can cause visible jumps. GSAP 3.10 added ignoreMobileResize to prevent ScrollTrigger.refresh() from firing on those small vertical resizes on touch-only devices—without changing the browser bar's behavior itself.

One final note: if the animation set is large enough to need separate mobile versions, build that in from the start. Retrofitting mobile layouts later—as Tom Miller puts it, "finalize all the animations before building"—is far more painful than planning for divergent layouts up front. gsap.matchMedia() lessens the sting, but it can't fully replace a responsive plan established on day one.