← Front-End Developer — all chapters  ·  Kelick Tech Hub

Chapter 40 — Animations and Motion Design

Section 1 — Why motion matters

Motion on a screen is not decoration added after a page is built; it is a form of communication. When a panel slides open, a list item fades in, or a button responds to a press, motion tells the user what just happened, what changed, and where their attention should go next. Without motion, changes appear instantly, and the user must work out for themselves what is new, what disappeared, and what they did. With well-chosen motion, those questions answer themselves, because the eye follows movement naturally and the brain treats a moving element as the same element in a new place, rather than as a new element that appeared from nowhere.

That tracking is the core value of motion. Human vision is highly sensitive to movement, which is why a small animated indicator can draw attention more effectively than a large static label. Designers use that sensitivity deliberately: a new message slides into a list, a selected tab's indicator glides to its new position, and a deleted item collapses before vanishing, so the user sees it leave rather than wondering where it went. Each of those motions preserves continuity. The user does not need to re-scan the whole page to understand the change; the motion itself explains it, which reduces cognitive load and makes the interface feel responsive and alive.

Motion also provides feedback, which is essential for trust. When a user taps a button, they need to know the tap registered, especially on a touch screen where there is no physical click. A brief scale change, colour shift, or ripple confirms the action immediately, before the slower result, such as a server response, arrives. Without that feedback, users tap again, thinking the first tap failed, and may submit a form twice or trigger an action twice. Feedback motion does not need to be dramatic; in fact, the best feedback is often subtle, lasting a fraction of a second, because its job is confirmation, not celebration.

There are limits, and they are important. Motion that is too long, too large, or too frequent becomes a distraction, delaying the user's task and drawing attention away from content. Motion that moves large areas of the screen, flashes, or loops continuously can also cause discomfort or nausea for some readers, and can trigger seizures in rare cases if it flashes rapidly. That is why motion design is a discipline of restraint: every animation should have a purpose, a modest duration, and a clear end state, and users should be able to reduce motion through their system settings, which your code should respect.

A useful habit is to ask, for every animation you propose, what it helps the user understand or do. If the answer is that it looks impressive, it is probably not justified, or it should be shortened and softened until it serves a purpose. If the answer is that it shows where an element went, confirms an action, or guides attention to new content, it is likely worthwhile. Motion design is not about adding movement to a static page; it is about using movement sparingly, purposefully, and respectfully, so the interface feels alive without feeling restless.

Check: What does motion help a user understand when an element changes position?

Section 2 — CSS transitions

A CSS transition animates a property change over time, rather than letting it happen instantly. When an element's property changes, for example because a class is added, a hover state begins, or a style is changed by JavaScript, a transition tells the browser to interpolate between the old value and the new value over a duration, with an easing function shaping the speed. Without a transition, the change is immediate: a button's background snaps from one colour to another. With a transition, it blends smoothly, which looks more polished and gives the user a moment to perceive the change.

The transition property is a shorthand for four sub-properties: transition-property, which names what animates; transition-duration, which sets how long it takes; transition-timing-function, which sets the easing; and transition-delay, which sets a wait before starting. A common declaration looks like this:

.button {
  transition: background-color 0.2s ease, transform 0.2s ease;
}

That rule says background colour and transform each animate over two tenths of a second with an ease curve. You can also write transition: all 0.2s ease, but that is discouraged, because it animates every changing property, including ones you did not intend, such as layout properties that are expensive to animate. Naming properties explicitly keeps transitions predictable and performant, and it documents which changes are meant to be animated.

Transitions only work between values the browser can interpolate. Colours, lengths, opacity, and transforms interpolate well, because the browser can compute intermediate values. Properties such as display, which jumps between none and block with no intermediate state, cannot transition, which is why showing and hiding elements with transitions requires techniques such as animating opacity and visibility together, or using newer properties that allow discrete transitions. Similarly, transitioning between auto and a fixed height does not work directly, because auto is not a number the browser can interpolate from; that limitation leads to the height-animation workarounds covered later in this chapter.

A practical pattern for transitions is to define them on the base state, not on the hover or active state, so the animation runs both when entering and when leaving the state. If you put the transition only on the hover rule, the change into hover will animate, but the change back will snap, because the transition rule disappears with the hover state. Defining it on the base element ensures both directions animate, which feels more consistent. Also keep durations short for interactive feedback, typically between one tenth and three tenths of a second, because a button that takes half a second to respond feels sluggish, even if the animation itself is smooth.

Finally, test transitions with real interaction, not just by reading the code. Hover, focus, tap, and keyboard-activate the element, and watch both directions. Check that the transition does not delay the action itself: a transition should animate the visual change, not postpone the underlying action, such as navigation or submission. If an action feels slow, shorten the duration or remove the transition from that property, because responsiveness matters more than smoothness. A transition that makes an interface feel slower has failed, however beautiful it looks.

Check: Why should a transition be defined on the base state rather than only on the hover state?

Section 3 — Timing functions and easing

A timing function, often called easing, controls how an animation's progress is distributed over its duration. A linear timing function moves at a constant speed from start to finish. An ease function starts slowly, speeds up, and slows down at the end. An ease-out function starts fast and slows down, while an ease-in function starts slowly and speeds up. Each of those shapes feels different, and choosing the right one is a large part of making motion feel natural rather than mechanical. A linear movement can feel robotic, because real objects rarely move at a perfectly constant speed; they accelerate and decelerate.

CSS provides named timing functions, such as linear, ease, ease-in, ease-out, and ease-in-out, and a cubic-bezier function for custom curves. A cubic-bezier curve is defined by two control points, and it lets you design exactly how speed changes over time. For example, cubic-bezier(0.2, 0, 0, 1) is a gentle ease-out that starts quickly and settles softly, which suits many interface transitions. You do not need to memorise control points; you need to understand that the curve's steepness at any point is the speed at that moment, and that a curve that rises steeply early means fast early movement, while a curve that flattens late means a gentle landing.

Choosing an easing function depends on the motion's purpose. For an element entering the screen, ease-out usually feels best, because it arrives quickly and then settles, drawing attention without lingering. For an element leaving, ease-in can feel appropriate, because it accelerates away, but ease-in can also feel sluggish at the start, so many designers use ease-in-out or a short ease-out for exits as well. For a continuous or looping motion, linear is often correct, because acceleration and deceleration at each loop boundary would create a noticeable pulse. For feedback, such as a button press, a very short ease-out gives a snappy response, while a long ease-in would feel delayed and unresponsive.

Steps timing functions are a special case. The steps function divides an animation into discrete jumps, with no interpolation between them, which is useful for frame-by-frame effects, such as a sprite animation or a typewriter effect where text appears character by character. Steps with a jump at the end or start controls whether the change happens at the beginning or end of each step. Most interface motion should not use steps, because smooth interpolation is what makes it feel continuous, but steps are the right tool when the motion is deliberately discrete, such as a clock ticking or a progress indicator advancing in chunks.

A common mistake is to use the same easing for everything, usually the default ease, without thinking about what each motion should feel like. Another mistake is to use an overly dramatic easing, such as a strong bounce, for routine transitions, which quickly becomes tiresome. A good practice is to define a small set of easing tokens for your project, such as one for entrances, one for exits, and one for feedback, and to use them consistently, so motion feels coherent across the interface. Test easing by watching the motion at real speed, not in slow motion, because an easing curve that looks interesting in a graph may feel wrong in use, and the only reliable test is the eye.

Check: Why does ease-out usually suit an element entering the screen?

Section 4 — CSS keyframe animations

While a transition animates between two states, a keyframe animation can animate through many states, with full control over intermediate steps. A keyframe animation is defined with the @keyframes rule, which names the animation and lists percentage points, each with the styles that should apply at that point in the animation. The element then uses the animation property to apply that keyframe set, with a duration, timing function, delay, iteration count, and other settings. That structure makes keyframes ideal for complex motions, such as a loading spinner that rotates continuously, a badge that pulses, or an element that moves, scales, and fades in sequence.

A simple keyframe animation looks like this:

@keyframes fade-up {
  from { opacity: 0; transform: translateY(8px); }
  to { opacity: 1; transform: translateY(0); }
}
.card { animation: fade-up 0.4s ease-out both; }

The from and to keywords are shorthand for zero percent and one hundred percent, and you can add intermediate percentages, such as fifty percent, for multi-step motions. The both value for animation-fill-mode, discussed in the next section, means the element holds the starting styles before the animation and the ending styles after it. Without a fill mode, the element would snap back to its unanimated styles after the animation ends, which is often not what you want for an entrance animation.

Keyframes have advantages over transitions for certain jobs. They can run automatically when a page loads, without waiting for a state change, which transitions cannot do. They can loop, reverse, alternate, and run multiple times, which transitions cannot do without JavaScript toggling classes. They can animate through a sequence, such as an element that slides in, pauses, and then fades, which a single transition cannot express. Those capabilities make keyframes the right tool for loaders, attention-drawing effects, and choreographed entrances, where the motion is not merely a response to a state change but a designed sequence in its own right.

They also have costs. Keyframe animations defined in CSS are harder to control dynamically than JavaScript animations: changing their duration or target based on data requires updating CSS variables or classes, rather than simply passing numbers to a function. They can also be harder to interrupt cleanly, because an animation in progress may need to be cancelled and replaced, which requires care with fill modes and class removal. For simple two-state changes, transitions are usually simpler and more maintainable than keyframes, because they respond directly to state changes without extra animation definitions.

A practical guideline is to use transitions for state changes, such as hover, focus, and toggling, and keyframes for sequences and loops, such as entrances, loaders, and repeated effects. Keep keyframe definitions small and named by purpose, such as fade-up or spin, rather than by element, so they can be reused. And keep their durations modest: an entrance that takes more than about half a second will delay the user's ability to use the content, however elegant it looks. Keyframes give you power over time; use that power to guide attention, not to show off.

Check: What can a keyframe animation do that a transition cannot?

Section 5 — Animation properties in detail

The animation shorthand packs many settings into one declaration, and understanding each one gives you control over how an animation behaves. The animation-name identifies the keyframes to use. The animation-duration sets how long one cycle takes. The animation-timing-function sets the easing within each cycle. The animation-delay sets how long to wait before starting, which can be negative, meaning the animation starts partway through, as if it had already been running. The animation-iteration-count sets how many times it runs, which can be a number or infinite for a loop. The animation-direction sets whether it runs forwards, backwards, or alternates.

Two properties deserve special attention: fill-mode and play-state. Fill-mode controls what styles apply before and after the animation. The value none means no styles are applied outside the animation's active period, so the element snaps back. Forwards means the final keyframe styles persist after the animation ends, which is what you usually want for an entrance. Backwards means the starting keyframe styles apply during the delay, so the element does not flash its final state before starting. Both means both forwards and backwards apply, which is a safe default for many entrances. Without the correct fill-mode, an animation can appear to work but then jump, which is a common and confusing bug.

Play-state controls whether an animation is running or paused. Setting animation-play-state to paused freezes the animation at its current point, which is useful for pausing a loader, for hover-to-pause effects, or for accessibility controls that let users stop motion. Combined with JavaScript, play-state lets you build pause buttons for animated content, which is a valuable accessibility feature for readers who find continuous motion distracting. Note that pausing does not reset the animation; resuming continues from where it paused, which is usually the desired behaviour.

Direction and iteration interact in ways worth knowing. With direction alternate, the animation runs forwards, then backwards, then forwards, and so on, which is useful for gentle pulsing or floating effects that should not snap back to the start. With direction reverse, the animation runs from its final keyframe to its first, which can be used for an exit that mirrors an entrance. Iteration count infinite should be used sparingly, because a looping animation that never stops can distract from content and consume attention; reserve infinite loops for loaders and indicators where continuous motion itself is the message, and provide a way to stop them when their job is done.

Finally, remember that the animation shorthand resets any sub-properties not listed to their defaults, which can cause surprises if you set a sub-property separately and then use the shorthand afterwards. To avoid that, set all animation settings in one shorthand declaration, or use longhand properties consistently. Also test animations in both directions and after interruption: start an animation, trigger it again quickly, and confirm it behaves sensibly rather than jumping or stacking. Animation properties are powerful, but their interactions reward deliberate testing, not just correct syntax.

Check: What does animation-fill-mode: both do, and why is it useful for entrances?

Section 6 — Transforms for motion

Transforms change an element's position, size, rotation, or skew without changing its layout, which makes them the ideal tool for animation. The transform property accepts functions such as translate, scale, rotate, and skew, which can be combined in one declaration. For example, transform: translateX(20px) scale(1.1) moves an element twenty pixels right and enlarges it by ten percent. Because transforms do not affect the layout of surrounding elements, animating a transform does not trigger a re-layout of the page, which is why transform animations are smooth and performant, as later sections explain in detail.

Translation is the most common transform for motion. TranslateX moves horizontally, translateY moves vertically, and translate moves in both directions. Translations are used for sliding panels, moving indicators, and entrance animations where an element fades and rises into place. A small translateY, such as eight pixels, combined with a fade, is a classic entrance that feels gentle and modern. Larger translations, such as sliding a drawer from off-screen, are used for panels and menus, where the element's start and end positions are far apart and the motion helps the user track where it came from.

Scale changes size, with scale(1) as the original size, scale(1.1) as ten percent larger, and scale(0.9) as ten percent smaller. Scale is used for hover feedback, where a card or button grows slightly, for press feedback, where it shrinks briefly, and for entrance effects, where an element grows from slightly smaller to full size. Scale should be used subtly for feedback, because a large scale change can feel jarring and can make text blurry during the animation, especially on low-density screens. Also remember that scaling an element does not change its layout size, so surrounding elements do not move, which is usually desirable for feedback but can cause overlap if the scale is large.

Rotation turns an element around a point, with rotate(45deg) turning it forty-five degrees clockwise. Rotation is used for spinners, for icons that indicate state, such as a chevron that rotates when a section expands, and for playful effects, such as a card that tilts on hover. Rotation is measured in degrees, and it can be combined with translation and scale for more complex motions. A rotating element's corners may extend beyond its original box, so check for clipping by parent elements with overflow hidden, which can cut off a rotating element unexpectedly.

The order of transform functions matters, because transforms are applied in sequence, and changing the order changes the result. Translating then rotating is not the same as rotating then translating, because rotation changes the direction of subsequent translations. A practical habit is to keep transform lists short and consistent, and to test combined transforms visually rather than assuming their result. Also note that transforms create a new containing block and stacking context, which can affect positioning of absolutely positioned children and z-index behaviour, so a transformed parent may change how its children behave in ways that surprise you if you are not expecting them.

Check: Why does animating a transform not disturb the layout of surrounding elements?

Section 7 — Transform origin and perspective

Transform origin is the point around which a transform happens, and changing it changes the feel of the motion dramatically. By default, transforms happen around the centre of the element, so a scale grows equally in all directions and a rotation spins around the centre. Setting transform-origin to a corner, an edge, or a specific coordinate changes that pivot: a scale from the top-left corner grows down and right, a rotation from the bottom edge swings like a door, and a scale from the bottom centre grows upwards like a bar chart. Choosing the origin to match the motion's meaning makes the motion feel intentional rather than arbitrary.

A common use is for elements that expand from a point, such as a menu that grows from its trigger button, or a tooltip that appears from its anchor. Setting the origin to the anchor side makes the expansion feel connected to its source, rather than growing from the centre and appearing disconnected. Another use is for bars and indicators that should grow from a baseline: setting the origin to the bottom means a vertical scale animation grows upwards from the baseline, which matches how bars are read, while the default centre origin would grow both up and down, which looks wrong for a bar. Always ask where the motion should pivot from, and set the origin accordingly.

Perspective adds depth to transforms, allowing three-dimensional rotations that would otherwise look flat. The perspective property, set on a parent, defines how far the viewer is from the plane, and smaller values create stronger depth effects. With perspective, rotateX and rotateY tilt an element in three dimensions, so it appears to turn away from or towards the viewer, rather than simply spinning in the plane. That is used for card flips, where a card rotates around its vertical axis to reveal its back, and for subtle tilts that respond to pointer movement. Without perspective, three-dimensional rotations look squashed, because there is no depth cue to interpret them.

Three-dimensional transforms also introduce backface-visibility, which controls whether the back of an element is visible when it is turned away. For a card flip, you typically want the back face hidden when it faces away, and the front face hidden when the card is flipped, which is achieved by setting backface-visibility to hidden on both faces and rotating one face by one hundred and eighty degrees initially. That construction is a classic pattern, and it is worth learning once, because it recurs in many interfaces. Test card flips on real devices, because three-dimensional transforms can be more expensive to render and can look different on different browsers.

A caution is that transform origin and perspective are easy to set incorrectly and hard to debug by reading code alone, because their effects are visual and depend on the element's size and position. Use developer tools to inspect the origin, try different values live, and watch the result. Keep origins consistent within a component, so similar elements behave similarly, and document any unusual origin choice, because a future maintainer may otherwise "fix" it back to the default and break the intended motion. Origin is a small property with a large effect on how motion reads.

Check: Why would a bar chart bar use transform-origin: bottom for a grow animation?

Section 8 — Opacity, visibility, and fading

Opacity and visibility are the two properties most often used for fading, and they behave differently in ways that matter for animation. Opacity sets how transparent an element is, from zero, fully transparent, to one, fully opaque, and it animates smoothly, because the browser can interpolate intermediate values. Visibility sets whether an element is visible or hidden, with values visible, hidden, and collapse, and it does not interpolate in the same way, but it does affect whether the element can be interacted with or read by assistive technology. Fading an element out with opacity alone leaves it invisible but still present, still occupying layout space, and still focusable, which can confuse keyboard users who tab to an invisible element.

The standard pattern for fading an element out and removing it from interaction is to animate opacity to zero, and then set visibility to hidden, or display to none, at the end. With transitions, that can be done by transitioning both opacity and visibility, with visibility delayed until the fade completes, so the element remains visible during the fade and becomes hidden only when fully transparent. With keyframes, the final keyframe can set visibility to hidden. That combination gives a smooth fade and a clean end state, where the element no longer receives focus or pointer events. The reverse, fading in, sets visibility to visible immediately and animates opacity from zero to one.

A related property is pointer-events, which can disable interaction without hiding an element, useful during a transition when you do not want clicks to land on a fading element. However, pointer-events does not affect keyboard focus, so it is not a substitute for visibility or for managing focus properly. When an element fades out, you should also move focus to a sensible place, such as the element that triggered the fade, or the next visible element, rather than leaving focus on an invisible element. That focus management is part of motion design, not an extra, because motion that strands keyboard focus creates a broken experience for keyboard users.

Fading is often combined with a small transform, such as a rise or a scale, to make entrances feel more alive than a pure fade. A pure fade can feel flat, because the element appears in place without any sense of arrival, while a fade plus a small rise gives a sense of emergence. Keep the transform small, such as eight pixels, so the motion remains subtle, and keep the duration short, so the content becomes usable quickly. For exits, the reverse, a fade plus a small fall or shrink, can signal departure without drawing excessive attention.

Finally, be careful with opacity on parent elements, because opacity affects all children and creates a stacking context, which can change how overlapping elements render. Also note that animating opacity is performant, because it does not trigger layout, which is why fades are among the cheapest animations. That performance, combined with their subtlety, makes fades a safe default for many transitions, provided you handle visibility and focus correctly. A fade that leaves an invisible, focusable element behind is not a finished animation; it is a bug waiting for a keyboard user to find it.

Check: Why is animating opacity alone not enough to hide an element properly?

Section 9 — What not to animate

Not all properties are equal for animation, and choosing the wrong property can make an otherwise simple animation slow and janky. Properties fall into three broad categories by what the browser must do when they change: layout properties, paint properties, and composite properties. Layout properties, such as width, height, margin, top, and left, force the browser to recalculate the layout of the page, which can be expensive, especially if many elements are affected. Paint properties, such as background colour, box shadow, and border, force the browser to repaint the element, which is less expensive than layout but still costly. Composite properties, such as transform and opacity, can be handled by the compositor, often on the graphics processor, without layout or paint, which makes them the fastest and smoothest to animate.

That hierarchy is the reason for the standard advice: animate transform and opacity where possible, and avoid animating width, height, top, left, and margin. A panel that slides in by animating left will trigger layout on every frame, while the same panel sliding in by animating transform translateX will not, and the difference is visible as smoothness, especially on a phone. Similarly, a card that grows by animating width and height will trigger layout, while one that grows by animating transform scale will not, although scale has its own trade-offs, such as not changing layout size, which may or may not be what you want.

There are exceptions and nuances. Sometimes a layout change is genuinely needed, such as an accordion expanding to reveal content that should push other content down, which a transform cannot do, because transforms do not affect layout. In those cases, you must animate a layout property or use a technique that approximates it, and you should accept the cost, minimise it by animating only the necessary element, and keep the animation short. The rule is not never animate layout, but prefer composite properties when the visual result is the same, and animate layout only when the layout change itself is the point of the animation.

Paint properties also deserve care. Animating box-shadow, for example, can be expensive, because shadows must be repainted on every frame, and large shadows are particularly costly. A cheaper alternative is to animate opacity between two pre-rendered shadow states, or to use a pseudo-element with the shadow and animate its opacity. Similarly, animating a gradient background is expensive and often not smooth, because gradients are repainted, not interpolated as a single value. For those effects, consider whether the animation is necessary at all, or whether a simpler transform or opacity change would convey the same meaning with far less cost.

A practical way to check is to use the browser's performance tools, which can show whether an animation triggers layout, paint, or composite, and how long each frame takes. If frames exceed about sixteen milliseconds, the animation will drop below sixty frames per second and appear janky. Profile before optimising blindly, because the culprit is often a property you did not expect, such as a shadow or a filter, rather than the property you thought you were animating. The goal is not to memorise a list of properties, but to understand the pipeline and to test your animations against it.

Check: Why is animating transform smoother than animating left or width?

Section 10 — Performance and sixty frames per second

Smooth animation is usually described as sixty frames per second, which means each frame has about sixteen milliseconds to be produced. That budget includes JavaScript execution, style calculation, layout, paint, and compositing, all of which must complete before the next frame is shown. If any of those stages takes too long, a frame is missed, and the animation appears to stutter or jump, which users perceive as jank. Understanding that budget helps you prioritise: the fewer stages your animation triggers, and the less work each stage does, the more likely you are to stay within budget, especially on slower devices.

The browser's rendering pipeline is the key mental model. First, JavaScript runs and may change styles or the document. Then the browser recalculates styles, determining which rules apply. Then it performs layout, calculating positions and sizes. Then it paints, drawing pixels into layers. Then it composites, combining layers into the final image. Animating transform and opacity can skip layout and paint for the animated element, because the compositor can move or fade an already-painted layer, which is why those animations are fast. Animating a layout property forces the pipeline to run from layout onwards on every frame, which is why those animations are slow. That model explains most animation performance advice in one picture.

Measuring performance is more reliable than guessing. Browser developer tools include a performance panel that records frame times, and a rendering panel that can highlight paint areas, show frame rates, and flag layout shifts. Use those tools on your animations, especially on a phone or with throttled processor settings, because a desktop machine may hide performance problems that a phone reveals. Look for long tasks, for frames that exceed sixteen milliseconds, and for paint areas that cover large parts of the screen when only a small element is animating. Those signals point to the specific problem, whether it is a layout property, a large shadow, or JavaScript work competing with animation.

JavaScript work can also steal the animation budget. If a frame's JavaScript takes ten milliseconds, only six remain for the rest of the pipeline, which may not be enough. That is why animations driven by JavaScript should keep per-frame work minimal, avoid layout reads that force synchronous layout, and avoid creating objects or measuring text in the frame loop. It is also why requestAnimationFrame, covered later, is preferred over timers for JavaScript animation, because it aligns work with the browser's repaint cycle, rather than competing with it.

Finally, remember that performance is user experience, not a technical nicety. A janky animation is worse than no animation, because it draws attention to itself for the wrong reason and makes the interface feel broken. If an animation cannot run smoothly on the devices your users actually use, simplify it, shorten it, or remove it, rather than shipping a stuttering effect. Smoothness is not about achieving sixty frames per second as a badge; it is about motion that feels effortless and does not distract from the task, which is the standard every animation in this chapter should be judged by.

Check: What happens when a frame takes longer than about sixteen milliseconds to produce?

Section 11 — Easing in practice

Easing curves become concrete when you compare them side by side on the same motion. Take a simple slide: an element moving one hundred pixels to the right over half a second. With linear easing, it moves twenty pixels every tenth of a second, steadily. With ease-out, it might move forty pixels in the first tenth, then thirty, then twenty, then ten, arriving quickly and settling gently. With ease-in, it might move ten, then twenty, then thirty, then forty, starting hesitantly and rushing at the end. Those different distributions feel different, even though the distance and duration are identical, because the eye is sensitive to acceleration and to how a motion ends.

In practice, most interface motion uses ease-out or a custom ease-out curve, because arriving gently feels like settling, while starting quickly feels responsive. A modal that slides in with ease-out appears promptly and then softens into place, which is reassuring. A modal that slides in with ease-in appears to hesitate, then rush, which can feel anxious or delayed, even if the duration is the same.

Custom cubic-bezier curves let you tune easing beyond the named functions. A curve such as cubic-bezier(0.16, 1, 0.3, 1), which is a strong ease-out with a long, soft tail, is popular for entrances, because it moves quickly at first and then eases for a long time, giving a sense of weight and polish. A curve with a slight overshoot, where the value goes beyond its target and then settles back, can give a playful, springy feel, but overshoot must be used carefully, because it can look like a bug if the element visibly passes its target and returns. Overshoot is best reserved for small, playful elements, such as icons or badges, not for large panels or text, where it distracts from reading.

Duration and easing interact. A short animation with a strong ease-out can feel snappy, while the same easing over a long duration can feel floaty and slow. A long animation with linear easing can feel mechanical, while a short linear animation can feel abrupt. When tuning, change one variable at a time: fix the duration, try several easings, pick the best, then adjust duration if needed. Changing both at once makes it hard to know which change improved the feel. Also test easing on different motions: an easing that suits a slide may not suit a fade, because fades have no spatial acceleration to perceive, and their easing affects perceived brightness change rather than speed.

A final practical point is consistency through tokens. Define a few easing curves for your project, name them by purpose, such as standard, entrance, and exit, and use them consistently, so motion across the interface feels like it belongs to one product. Inconsistent easing, where one panel eases out, another eases in, and a third is linear, makes the interface feel assembled from unrelated parts, even if each animation is individually fine. Tokens, combined with duration tokens, give motion a system, which is the subject of later sections on motion design tokens and on design systems in the next chapter.

Check: Why does changing duration and easing at the same time make tuning harder?

Section 12 — Stagger and sequencing

Stagger is the technique of starting similar animations at slightly offset times, so elements appear in sequence rather than all at once. When a list loads, for example, its items might fade and rise one after another, with each item delayed by fifty milliseconds from the previous one. That sequence guides the eye down the list, gives a sense of order, and makes the list feel alive, whereas all items appearing at once can feel abrupt, and items appearing in a random order can feel chaotic. Stagger is one of the most effective ways to make a simple entrance feel designed, because it adds rhythm without adding complexity to any single element's motion.

Implementing stagger in CSS is straightforward with animation-delay or transition-delay set per element, often using an inline style or a nth-child rule that increments the delay. For a list of five items with a fifty-millisecond stagger, delays might be zero, fifty, one hundred, one hundred and fifty, and two hundred milliseconds. Keep the stagger small, because a large stagger delays the last items significantly, and a user waiting for the tenth item in a list to appear will perceive the interface as slow, even if each item's animation is short. A total stagger across a list should usually stay under about half a second, so the whole list is usable quickly, even if it does not all appear at once.

Sequencing is the broader idea behind stagger: ordering motions so they tell a story or guide attention. A modal might fade in its backdrop first, then slide in its panel, then fade in its content, each step building on the previous one. A form submission might show a button state change, then a success message, then a navigation, in that order, rather than all at once. Sequencing helps users follow a process, because each step's motion signals its place in the sequence. Without sequencing, multiple simultaneous motions compete for attention, and the user may miss the most important one, because the eye cannot follow several moving elements at once.

Stagger and sequencing also have risks. Too much stagger can feel like a delay, especially for content the user needs immediately, such as search results or error messages, which should appear promptly rather than in a choreographed sequence. Too much sequencing can make a simple action feel ceremonial, as if the interface is showing off rather than serving the user. A good rule is to stagger decorative or secondary content, such as list items or cards, but to show primary content and feedback promptly, with minimal delay. If a user is waiting for an answer, give it to them; do not make them watch it arrive in stages.

Accessibility applies here too. Staggered animations can be distracting for readers who prefer reduced motion, so respect that setting by showing all items immediately without stagger when it is active. Also ensure that staggered content is available to screen readers in a sensible order, regardless of visual stagger, because visual order and document order should agree, and stagger should not delay the availability of content to assistive technology. Stagger is a visual rhythm, not a content delay, and treating it as such keeps it a refinement rather than a barrier.

Check: Why should total stagger across a list stay under about half a second?

Section 13 — Hover and focus micro-interactions

Micro-interactions are small, purposeful animations that respond to a single user action, such as hovering, focusing, or pressing an element. They are the most common animations in interfaces, and they are also the most judged, because users encounter them constantly. A good micro-interaction confirms the action, hints at what will happen, and feels immediate. A button that darkens slightly on hover, lifts slightly on focus, and shrinks briefly on press gives three distinct, useful signals with minimal motion, and each signal helps the user understand the element's state without reading any text.

Hover interactions should be subtle and fast. A hover effect that takes half a second to appear feels delayed, because the user has already moved on by the time it arrives. Aim for hover transitions of around one to two tenths of a second, with a simple property change, such as background colour, border colour, or a small transform. Hover should also be reversible: when the pointer leaves, the element should return smoothly to its base state, not snap back, which is why transitions should be defined on the base state, as earlier sections explain. Avoid hover effects that change layout, such as growing an element's size in a way that pushes neighbours, because that shift can cause the pointer to leave the element and trigger a flicker loop.

Focus interactions are as important as hover, but they are often neglected. Keyboard users rely on focus styles to know where they are, and a focus style that is animated, such as a ring that fades or expands in, can be both clear and pleasant, provided it remains highly visible. Do not remove the default focus outline without replacing it with an equally clear custom style, because that makes keyboard navigation invisible. A focus style should have strong contrast, should not rely on colour alone, and should appear immediately on focus, not after a delay, because a delayed focus indicator leaves the keyboard user temporarily lost.

Press, or active, interactions confirm that an action has been received. A button that scales to ninety-eight percent on press, or that darkens briefly, tells the user their tap registered, even before the action's result appears. Press feedback should be very short, around one tenth of a second, because a long press animation feels like the button is stuck. It should also be distinct from hover and focus, so the user can tell the difference between hovering over a button, focusing it, and actually pressing it. Those three states, hover, focus, and active, together form the basic language of interactive elements, and getting them right is more valuable than any elaborate animation elsewhere.

Finally, remember that hover does not exist on touch devices in the same way, so hover-only information, such as a tooltip that appears only on hover, will be missed by touch users. Micro-interactions should enhance, not carry, essential information: a button's label should be clear without hover, and its hover effect should be a bonus, not a requirement for understanding. Test micro-interactions with a mouse, a keyboard, and a touch screen, and confirm each input method gets appropriate feedback. An interface whose micro-interactions work for only one input method is incomplete, however polished it looks with that method.

Check: Why must a focus style appear immediately rather than after a delay?

Section 14 — Button and control feedbackButtons are the most interacted-with elements in most interfaces, so their feedback deserves detailed attention. A button's feedback has several stages: its resting state, its hover state, its focus state, its active or pressed state, its disabled state, and its loading state. Each stage should be visually distinct, so the user always knows what state the button is in, and transitions between stages should be smooth but fast. A button that looks identical in all states gives no feedback, while a button whose states change dramatically can feel unstable.

The resting state is the button's default appearance, with its label, background, and border. The hover state typically darkens the background or lightens it, depending on the theme, and may add a subtle shadow or lift, signalling that the button is interactive and under the pointer. The focus state adds a visible ring or outline, distinct from hover, so keyboard users can see it. The active state, when the button is pressed, typically darkens further or scales slightly down, signalling that the press has been received. Those four states should be designed together, with consistent colours and transitions, so the button feels like one element changing state, not four different elements swapping places.

Disabled and loading states are often overlooked, but they are crucial for preventing errors. A disabled button should look clearly inactive, usually with reduced opacity and a muted colour, and it should not respond to hover or press feedback that implies it can be clicked. A loading button should show that work is in progress, often by replacing its label with a spinner or by adding a spinner beside the label, and it should be disabled during loading to prevent duplicate submissions. The transition into loading should be smooth, not a sudden swap that confuses the user about whether their click registered. Also, loading text should be descriptive, such as "Saving", rather than just a spinner, so screen readers and users who cannot see the spinner understand what is happening.

Feedback timing matters as much as appearance. Hover and focus transitions should be around one to two tenths of a second, press feedback around one tenth, and loading transitions should be immediate, because the user needs to know instantly that their action was received. A button that takes half a second to show loading feedback will prompt the user to click again, which is exactly what loading feedback is meant to prevent. Similarly, a button that stays in its loading state long after the action completes feels broken, so the loading state must be cleared reliably, even when the action fails, with an error state or message to explain what happened.

Finally, test button feedback in all states, with all input methods, and in all themes, including dark mode, where hover and disabled colours may need adjustment. Check contrast for each state, because a hover colour that looks fine in light mode may fail in dark mode, and a disabled button that is too pale may be unreadable. Buttons are small, but they carry a large share of user trust: a button that responds clearly, promptly, and consistently in every state makes the whole interface feel reliable, while a button that confuses in any state undermines that trust disproportionately.

Check: Why should a loading button be disabled during loading?

Section 15 — Page and view transitions

Page transitions animate the change from one view to another, such as navigating between pages in a single-page application, or switching tabs in an interface. Without a transition, the new view appears instantly, and the user must re-orient themselves: where am I, what is this view, and how does it relate to the previous one? With a transition, the new view slides, fades, or scales in, and the old view slides, fades, or scales out, giving the user a sense of movement through the interface, as if they travelled from one place to another, rather than teleporting. That sense of place helps users build a mental map of the application.

The simplest page transition is a fade: the old view fades out, the new view fades in, often with a brief overlap. Fades are gentle, work for any content, and rarely distract, which makes them a safe default. Slide transitions, where the new view slides in from a direction, can suggest navigation hierarchy: sliding in from the right can suggest going deeper, while sliding in from the left can suggest going back, mirroring common mobile patterns. Scale transitions, where the new view grows from a point, can suggest emergence from a trigger, such as a card expanding to a detail view.

Implementing page transitions requires care with timing and with the outgoing view. In a single-page application, the outgoing view is often removed immediately, so its exit animation cannot play unless the code keeps it mounted during the transition and removes it afterwards. That lifecycle management is the main technical challenge, and it is why many applications use a transition library or a routing library with transition support, rather than hand-rolling the lifecycle. The incoming view's entrance animation is simpler, because it can play on mount, but it must not delay the content's availability: the view should be usable as soon as possible, even if its entrance animation is still playing, and focus should move to the new view's main heading or first interactive element promptly.

Duration for page transitions should be modest, typically between two and four tenths of a second, because a longer transition delays the user's access to the new view and can feel like a loading screen. For frequent navigation, such as switching tabs, even shorter durations, around two tenths, are better, because the user may switch repeatedly and will quickly tire of a slow transition. For less frequent, more significant navigation, such as opening a detail view, a slightly longer transition can help orient the user, but it should still stay under half a second. If a transition feels like waiting, it is too long, regardless of how elegant it looks.

Accessibility for page transitions includes respecting reduced motion, by using a simple fade or no transition when that setting is active, and managing focus and announcements, so screen reader users know the view changed. A view change should update the page title, move focus sensibly, and, where appropriate, announce the new view's heading, because a visual transition alone does not communicate the change to a screen reader user. Page transitions are about orientation, and orientation must be available to all users, not only to those who can see the motion.

Check: Why must an outgoing view stay mounted during its exit animation?

Section 16 — Loading states and skeletons

Loading states tell the user that work is happening and that they should wait, and their design affects perceived performance as much as actual performance. A spinner is the simplest loading indicator: a rotating element that signals activity, but gives no information about how much work remains or what is loading. A progress bar gives more information, showing a proportion complete, but only when progress can be measured; a fake progress bar that advances arbitrarily can mislead, because it implies knowledge it does not have. A skeleton screen, which shows placeholder shapes resembling the eventual content's layout, gives the most context, because it preserves layout and hints at what is coming, reducing the jarring shift when content arrives.

Skeleton screens work best for content with a predictable layout, such as a list, a card grid, or an article, where placeholders can mirror the real structure. A skeleton for a list might show grey bars for text lines and circles for avatars, in the same positions as the real items will occupy. That mirroring means the page's layout does not jump when data arrives, which is less disorienting than a spinner that is replaced by content of a different height.

Spinners remain appropriate for short, indeterminate waits, such as a button action or a small data fetch, where a full skeleton would be disproportionate. A spinner should be labelled, either with visible text, such as "Loading", or with an accessible label for screen readers, because a spinning element alone does not announce itself clearly. It should also be sized appropriately: a tiny spinner in a large empty area looks lost, while a huge spinner for a button action looks alarming. Place the spinner where the content will appear, or beside the triggering control, so the user connects the wait with its cause, rather than searching the page for what is loading.

A common mistake is to show a loading state for a wait so short that it flashes on screen and disappears, which feels like a glitch rather than feedback. For very short waits, it is often better to show nothing, or to delay the loading indicator by a brief threshold, such as two hundred milliseconds, so it only appears if the wait is genuinely noticeable. Conversely, for long waits, a loading state should not spin indefinitely without explanation; it should offer progress, a message, or a way to cancel, because an indefinite spinner leaves the user wondering whether the page has frozen. Those thresholds and messages are part of loading design, not just implementation details.

Finally, loading states must be accessible. A loading message should be announced to screen readers, but not repeatedly, because repeated announcements for every skeleton or spinner update can be noisy. A single polite announcement, such as "Loading results", followed by an announcement when content arrives, is usually sufficient. Also ensure that focus does not get trapped in a loading area, and that the loaded content, when it arrives, is reachable by keyboard in a sensible order. Loading is a state, not an absence of design, and treating it as such makes waits feel shorter and less frustrating, even when the actual wait time is unchanged.

Check: Why is a skeleton screen often better than a spinner for a list of content?

Section 17 — Scroll-triggered animation

Scroll-triggered animation plays an animation when an element enters the viewport, or reaches a certain scroll position, rather than when the page loads. It is commonly used for content that appears as the user scrolls down a long page, such as sections fading and rising into view, images scaling in, or statistics counting up when they become visible. The appeal is that motion is tied to the user's own exploration: content animates as it is discovered, which can make a long page feel dynamic and guide attention to each section as it arrives, rather than animating everything at once on load, much of which the user would never see.

The modern way to implement scroll-triggered animation is the Intersection Observer API, which lets JavaScript watch elements and receive a callback when they intersect the viewport, without needing to listen to every scroll event and measure positions manually. Listening to scroll events directly can be expensive, because scroll events fire frequently and measuring layout on each one can cause jank, whereas Intersection Observer is handled efficiently by the browser. A typical pattern is to add a class, such as is-visible, when an element becomes visible, and to define a CSS transition or animation for that class, so the element animates from its hidden state to its visible state.

Thresholds and margins control when the animation triggers. A threshold of zero means the animation starts as soon as any part of the element is visible, while a threshold of one means it starts only when fully visible. A root margin can expand or shrink the observation area, so the animation starts slightly before the element enters view, or only after it is well inside. Starting slightly before entry, such as with a negative bottom margin, can make the animation feel timely, because it is already in progress when the element appears, rather than starting late and looking delayed. Test thresholds on real scroll speeds, because a fast scroll can skip past an element's trigger point in a way that a slow, deliberate scroll does not reveal.

Scroll-triggered animation has risks that must be managed. If content is hidden until it scrolls into view, and JavaScript fails or the observer does not fire, the content may remain hidden and inaccessible, which is a serious failure. To prevent that, hidden content should be visible by default in the absence of JavaScript, with the hidden state applied only by JavaScript when it is ready to animate, or the animation should be an enhancement that does not hide content at all, but merely adds motion to content that is already visible. That progressive enhancement approach ensures the page works even if animation fails, which is a fundamental principle for any scroll-triggered effect.

Also consider whether scroll-triggered animation is appropriate at all. For content the user needs to read, such as instructions or data, immediate visibility is better than a delayed entrance, because animation delays reading. For decorative or secondary content, such as illustrations or section dividers, scroll-triggered animation can add delight without harming usability. Respect reduced motion by disabling scroll-triggered animations when that setting is active, showing content immediately instead. Scroll-triggered motion should reward exploration, not gate content behind it.

Check: Why is Intersection Observer preferred over listening to scroll events directly?

Section 18 — Parallax and its cautions

Parallax is the effect where different layers of a page move at different speeds as the user scrolls, creating a sense of depth, as if nearer layers move faster than farther ones. It is often used for hero sections, where a background image moves more slowly than foreground text, giving the impression that the text floats over a distant scene. That depth can be visually striking and can make a page feel immersive, especially for storytelling or marketing pages where atmosphere matters. However, parallax is also one of the most misused motion techniques, because it can harm readability, performance, and accessibility if applied carelessly.

The technical implementation usually involves translating layers by different amounts based on scroll position, often with JavaScript updating transforms on scroll, or with CSS techniques that create layered scrolling contexts. JavaScript-driven parallax must be efficient, using requestAnimationFrame and transform updates, rather than layout properties, or it will cause jank, because scroll-driven updates happen frequently and any expensive work per scroll will be visible. CSS-only parallax, using perspective and transform on scrolling containers, can be smoother, but it is more complex to set up and can interact unexpectedly with other layout, such as sticky positioning or overflow. Whichever method you use, test on a phone, where scroll behaviour and performance differ from desktop, and where parallax often feels less impressive and more distracting.

Readability is the first caution. When a background moves independently of text, the text's contrast against the background changes as the user scrolls, because different parts of the image pass behind the text. A text block that was legible against a dark part of the image may become illegible against a light part, unless the text has a solid background, a shadow, or a scrim that maintains contrast throughout. Also, moving backgrounds can draw attention away from text, making reading harder, especially for long passages. Parallax is therefore best suited to short hero text over a background, not to body text that must be read carefully.

Accessibility is the second caution. Parallax motion can cause discomfort, dizziness, or nausea for some readers, particularly when large areas move at different speeds, because the visual system receives conflicting depth cues. It can also be distracting for readers with attention difficulties, and it can interfere with screen reader and keyboard navigation if it changes layout or focus order. For those reasons, parallax should be disabled entirely when prefers-reduced-motion is active, and it should be subtle even when enabled, with small movement differences rather than dramatic ones. If parallax is essential to a design, provide a way to turn it off, not just a system setting, because not all users who find it uncomfortable have that setting enabled.

A practical guideline is to use parallax sparingly, for a single hero section, with a small speed difference, and to test it with real users, including those who scroll quickly, those who use a keyboard, and those who prefer reduced motion. If the parallax does not clearly improve the page's communication, remove it, because its costs in performance, readability, and accessibility are real, while its benefits are often merely aesthetic. Depth can also be suggested with static layering, shadows, and scale, without any scroll-driven motion, which is a safer alternative for many pages.

Check: Why can parallax harm text readability as the user scrolls?

Section 19 — The Web Animations API

The Web Animations API is a JavaScript interface for creating and controlling animations directly, without relying solely on CSS. It lets you animate an element by calling its animate method with keyframes and timing options, and it returns an Animation object that you can play, pause, reverse, cancel, and listen to for completion. That programmatic control makes it powerful for animations whose values depend on data, user input, or calculations at runtime, such as animating an element to a position computed from another element's location, or animating a chart to new data values. CSS animations, by contrast, are defined statically, and changing their targets dynamically requires updating styles or variables, which is less direct.

A basic example looks like this:

element.animate(
  [{ opacity: 0, transform: "translateY(8px)" },
   { opacity: 1, transform: "translateY(0)" }],
  { duration: 400, easing: "ease-out", fill: "both" }
);

The first argument is an array of keyframes, each an object with CSS properties and values, and the second is a timing object with duration, easing, delay, iterations, direction, and fill, mirroring CSS animation properties. The returned Animation object can be stored, paused with pause, played with play, reversed with reverse, and finished with finish, which jumps to the end state. It also has a finished promise that resolves when the animation completes, which is useful for sequencing: you can await one animation's completion before starting the next, without nesting callbacks or timers.

The Web Animations API also unifies CSS animations and transitions under a common model, which means animations created in CSS can be inspected and controlled through the same API, via methods such as getAnimations. That unification is useful for debugging and for building controls, such as a pause-all button, that need to affect both CSS and JavaScript animations. It also means that the performance characteristics are similar: animations of transform and opacity through this API are compositor-friendly, just as in CSS, because they use the same underlying engine.

Compared with CSS, the Web Animations API is more verbose for simple cases, but more flexible for dynamic ones. A transition is simpler for a hover effect, and a CSS keyframe is simpler for a repeating loader, but a JavaScript animation is simpler when the target value is computed, when the animation must be interrupted and retargeted smoothly, or when multiple animations must be coordinated precisely. A common pattern is to use CSS for static, state-driven motion, and the Web Animations API for dynamic, data-driven motion, with each tool used where it is clearest. That division keeps code readable and avoids forcing either tool into a job it handles awkwardly.

Finally, the Web Animations API respects the same accessibility considerations as CSS: it does not automatically respect prefers-reduced-motion, so you must check that setting yourself and skip or shorten animations when it is active. It also does not automatically manage focus or announcements, so those remain your responsibility. The API gives you control over time, but it does not give you judgement about when motion is appropriate; that judgement, as throughout this chapter, comes from asking what the motion helps the user understand or do.

Check: What does the animate method return, and why is that useful?

Section 20 — requestAnimationFrame

RequestAnimationFrame is the browser's mechanism for running JavaScript animation code in sync with the screen's refresh cycle, and it is the foundation for most JavaScript-driven motion, including canvas animation from the previous chapter. You call requestAnimationFrame with a callback function, and the browser calls that function before the next repaint, passing it a timestamp. Inside the callback, you update the animated values, draw or set styles, and, if the animation is not finished, call requestAnimationFrame again to schedule the next frame. That loop continues until the animation completes, at which point you stop requesting frames.

The main advantage over timers, such as setInterval or setTimeout, is synchronisation. Timers fire on a fixed schedule, regardless of whether the browser is ready to paint, which can cause frames to be skipped or drawn twice, leading to jank. RequestAnimationFrame fires just before a repaint, so each update corresponds to a frame the user will actually see, and it automatically pauses when the tab is hidden or the element is off-screen, saving battery and processor time. It also adapts to the display's refresh rate, so an animation runs at sixty frames per second on a sixty-hertz screen and at higher rates on higher-refresh screens, without code changes, provided the animation is based on elapsed time rather than frame count.

Basing animation on elapsed time is essential, as with canvas animation. The callback receives a timestamp, and you compute progress as elapsed time divided by duration, clamped between zero and one, then apply easing and interpolate values. If you instead move a fixed amount per frame, the animation will run faster on high-refresh screens and slower on low-refresh screens, and it will slow down if frames are dropped, because fewer frames mean less movement. Elapsed-time animation, by contrast, catches up after a dropped frame, because the next frame's progress is based on time, not on how many frames have run, so the animation ends on time even if some frames were missed.

A typical loop looks like this in outline: record a start time on the first callback, compute elapsed on each callback, derive progress, update the element's transform or opacity, and request the next frame if progress is less than one. When progress reaches one, set the final values exactly and do not request another frame, so the loop stops cleanly. If the animation needs to be cancelled, store the request identifier returned by requestAnimationFrame and pass it to cancelAnimationFrame, which stops the loop without completing it. That cancellation is important for interrupted animations, such as when a user triggers a new animation before the old one finishes, or when a component is removed from the page.

Performance habits for requestAnimationFrame mirror those for CSS: keep per-frame work minimal, avoid layout reads that force synchronous layout, avoid creating objects in the loop, and prefer transform and opacity updates over layout properties. Also avoid running multiple independent loops for related animations; a single loop that updates several elements is more efficient and easier to coordinate than several loops competing for frame time. RequestAnimationFrame is a simple tool, but using it well, with elapsed time, clean stopping, and minimal per-frame work, is what separates smooth JavaScript motion from janky motion.

Check: Why should a requestAnimationFrame animation use elapsed time rather than a fixed amount per frame?

Section 21 — JavaScript timelines and sequencing

A timeline is a way of organising multiple animations in order, with precise control over when each starts, how long it lasts, and how they overlap. In simple JavaScript, a timeline can be built by chaining animations with promises, such as awaiting one animation's finished promise before starting the next, or by using a single requestAnimationFrame loop that updates several elements according to a shared clock. In more complex work, animation libraries provide timeline objects that let you add animations at specific times, with labels, offsets, and nesting, which is useful for choreographed sequences, such as an onboarding flow or a product tour, where several elements must move in a coordinated story.

Building a timeline by hand starts with a clock. Record a start time, and on each frame, compute elapsed time, then for each animation in the timeline, compute its local progress based on its start offset and duration: if elapsed is before its start, its progress is zero; if elapsed is after its end, its progress is one; otherwise, its progress is elapsed minus start, divided by duration, with easing applied. That calculation, repeated for each animation, gives you a timeline where animations can overlap, follow each other, or run in parallel, all driven by one loop. That single-loop approach is efficient and easy to reason about, because all animations share the same clock and the same stopping condition.

Sequencing with promises is simpler for linear sequences, where one animation follows another with no overlap. You can write code that awaits the first animation, then starts the second, then the third, which reads like a story and is easy to follow. That approach is less suitable for overlapping animations, because promises naturally express sequence, not overlap, and coordinating overlaps with promises requires manual delays and parallel promise handling, which quickly becomes confusing. For overlapping or complex timelines, a clock-based loop or a library timeline is clearer, because time is explicit rather than implied by promise order.

A key challenge in timelines is interruption. If a user dismisses a modal mid-entrance, or navigates away mid-sequence, the timeline must stop cleanly, without leaving elements in intermediate states or continuing to animate hidden elements. That requires a cancellation mechanism, such as a flag that the loop checks, or cancelling individual Animation objects, and a cleanup step that sets final or initial states explicitly. Without cleanup, interrupted timelines leave the interface in a half-animated state, which can look broken and can confuse focus and interaction. Design timelines with interruption in mind from the start, not as an afterthought, because users interrupt animations frequently, especially on mobile where taps are fast and navigation is frequent.

Finally, timelines should respect the same principles as single animations: keep total duration reasonable, respect reduced motion by skipping to the final state, and ensure that content is available even if the timeline fails or is interrupted. A timeline that gates content behind a long sequence will frustrate users who need that content quickly, so primary content should appear promptly, with timeline motion enhancing rather than delaying it. Timelines are powerful for storytelling and onboarding, where sequence itself is the message, but they should not be used to delay ordinary content, where sequence adds little and cost is high.

Check: How does a clock-based timeline handle animations that overlap?

Section 22 — Animating SVGSVG elements can be animated with CSS, with the Web Animations API, and, in some cases, with SMIL, the native SVG animation language, but CSS is the most common and most maintainable approach for most SVG motion. Because SVG elements are part of the document, CSS transitions and keyframes can animate their presentation properties, such as fill, stroke, opacity, and transform, just as for HTML elements.

Animating SVG transforms requires care with transform-origin and with the coordinate system. By default, SVG transform-origin is relative to the SVG viewport, not to the element's own box, unless transform-box is set to fill-box, which makes the origin relative to the element's bounding box. That distinction matters: a rotation that should spin around a shape's centre will instead spin around the viewport origin if transform-box is not set, producing a wild orbit rather than a spin. Setting transform-box: fill-box and transform-origin: center is a common fix for element-relative transforms in SVG, and it should be part of your SVG animation habit, tested in the browsers you support, because support and defaults have varied historically.

A classic SVG animation is the line-drawing effect, where a path appears to draw itself. It works by setting stroke-dasharray to the path's length, so the path is a single dash covering its whole length, and stroke-dashoffset to the same length, so the dash is shifted off the path and invisible, then animating dashoffset to zero, so the dash slides onto the path, revealing it progressively. To use it, you need the path's length, which can be obtained with the getTotalLength method in JavaScript, and set as a CSS variable or inline style. That technique is used for signatures, route drawings, and chart line entrances, and it is effective because it turns a static shape into a process the eye can follow.

SMIL, which is SVG's native animation elements such as animate and animateTransform, was once the standard way to animate SVG, but its use has declined, and it is deprecated in some contexts, with CSS and JavaScript animations preferred for new work. SMIL animations are declarative and can be written directly in SVG markup, which is elegant for simple cases, but they are harder to control from JavaScript, harder to pause and sequence with other animations, and less consistently supported. For new projects, prefer CSS or the Web Animations API for SVG motion, and treat SMIL as a legacy technique to recognise when reading old examples, not as a tool to reach for first.

Finally, SVG animation should follow the same performance and accessibility principles as HTML animation: animate transform and opacity where possible, keep durations short, respect reduced motion, and ensure that animated SVG content remains accessible, with titles, descriptions, and a text alternative where the SVG conveys information. An animated SVG illustration that is purely decorative should be hidden from assistive technology, while an animated SVG chart should have its data available as text, as in the previous chapter. Motion in SVG is powerful because it is element-based, but that power must be paired with the same restraint as elsewhere.

Check: Why must transform-box be set for element-relative SVG transforms?

Section 23 — Animating canvas

Canvas animation, introduced in the previous chapter for charts, follows the same requestAnimationFrame loop as other JavaScript motion, but with its own considerations because the whole picture is redrawn each frame. Each frame, you clear the canvas, update the animated values based on elapsed time, and redraw all shapes at their current positions, sizes, or colours. That full redraw is simple and flexible, because any property can change each frame, but it can also be expensive if the picture is large or complex, so canvas animation performance depends on keeping per-frame drawing work low, reusing gradients and patterns, and separating static layers from animated layers, as earlier sections advise.

A useful canvas animation pattern is to separate the scene into objects, each with its own position, velocity, or animation state, and to update each object per frame, then draw it. For example, a particle system might have many particles, each with a position, velocity, and life, updated by adding velocity to position and decreasing life, then drawn as a circle with opacity based on life. That object-based structure keeps animation code organised, because each object's update logic is local, and the main loop simply updates all objects and draws them in order. Without that structure, canvas animation code quickly becomes a tangle of global variables and drawing calls that are hard to debug or extend.

Interpolation and easing in canvas animation work as in other JavaScript animation: compute progress from elapsed time, apply an easing function, and interpolate between start and end values. For continuous motion, such as a moving object, update by velocity and time delta, rather than by fixed per-frame steps, so motion is smooth across different refresh rates and after dropped frames. Time delta is the time since the last frame, and multiplying velocity by delta gives movement that is independent of frame rate, which is the continuous equivalent of elapsed-time progress for finite animations. Both techniques share the same principle: time, not frame count, drives motion.

Canvas animation also needs handling for resizing, for visibility, and for cleanup. When the canvas resizes, the drawing coordinates and scales must be updated, and the animation should continue from its current state, not restart from scratch, unless a restart is intended. When the tab is hidden, requestAnimationFrame pauses automatically, which is beneficial, but the animation's clock should account for the pause, or it may jump when the tab becomes visible again, because elapsed time includes the hidden period. Using a clock that pauses with visibility, or clamping time delta to a maximum, prevents that jump. When the animation is no longer needed, cancel the frame request and clear any references, so the loop does not continue drawing a hidden canvas.

Accessibility for canvas animation follows the same rules as for canvas charts: provide a text alternative, respect reduced motion by showing a static final state, and avoid continuous, distracting motion that cannot be paused. A canvas animation that loops forever without a pause control can be particularly distracting, so provide a pause mechanism where the animation is not essential, and ensure that any information conveyed by the animation is also available statically. Canvas gives you full control over motion, but that control comes with full responsibility for performance, for time handling, and for accessibility.

Check: Why should canvas animation use time delta for continuous motion?

Section 24 — Motion and accessibility

Motion accessibility is not an optional extra; it is a core part of motion design, because motion affects readers differently. Some readers experience discomfort, dizziness, or nausea from large, fast, or parallax motion, a condition related to vestibular disorders, where the visual system and the balance system receive conflicting signals. Others find continuous motion distracting, making it hard to focus on text or to complete a task. Still others are sensitive to flashing, which can trigger seizures in rare cases. Designing motion without considering these effects can make an interface unusable for a significant minority, even if it looks impressive to others.

The primary tool for motion accessibility is the prefers-reduced-motion media query, which reflects a user's system setting indicating they prefer less motion. In CSS, you can use that query to disable or shorten animations and transitions, for example by setting animation-duration to a very short value, or animation to none, for elements that would otherwise animate. In JavaScript, you can check the same setting with matchMedia and skip or shorten animations accordingly. The goal is not to remove all motion, but to remove motion that is large, continuous, or distracting, while preserving essential feedback, such as a brief colour change that confirms an action, which can be shown instantly rather than animated.

A practical reduced-motion stylesheet often looks like this in outline: within a prefers-reduced-motion: reduce block, set animation-duration, animation-iteration-count, transition-duration, and scroll-behaviour to values that effectively disable motion, such as very short durations and a single iteration. That blanket approach is a good starting point, but it should be refined: essential state changes should still be clear, even if not animated, so a modal that would slide in should simply appear, and a loading spinner that would rotate should be replaced by a static indicator or text. The reduced-motion version should be fully usable, not a broken version of the animated design, which means testing it as a first-class design, not as an afterthought.

Beyond reduced motion, other accessibility considerations apply. Avoid flashing content that flashes more than three times per second, because that is a known seizure risk, and avoid large areas of rapidly changing colour or brightness. Provide controls to pause, stop, or hide moving content, especially for carousels, tickers, and background videos, because a user should be able to stop motion that distracts them, even if they have not set a system preference. Ensure that motion does not hide content or delay its availability, as with scroll-triggered animations that hide content until scrolled into view, because that can strand keyboard and screen reader users.

Finally, test motion accessibility with real settings and real users where possible. Turn on reduced motion in your system, and use your interface, confirming that all content is available, all feedback is clear, and no motion remains that should have been removed. Ask readers who are sensitive to motion what they experience, because their feedback will reveal problems that checklists miss. Motion that respects its readers is not less impressive; it is more professional, because it serves all users, not only those who enjoy motion.

Check: What should happen to a modal entrance animation when prefers-reduced-motion is active?

Section 25 — Motion for feedback and errors

Motion is particularly valuable for feedback, because it can draw attention to a change without requiring the user to search for it. When a form is submitted successfully, a brief success animation, such as a checkmark drawing itself or a message fading in, confirms the outcome and gives the user a moment of closure before they move on. When a form has errors, motion can guide the user to the problem: the first invalid field can be highlighted, scrolled into view, and given a brief shake or pulse, so the user notices it even if it is off-screen or among many fields. That guidance is more effective than a static error message alone, because motion draws the eye to the exact place that needs attention.

Error motion must be used carefully, because it can feel punitive if overdone. A violent shake, a flashing red border, or a large bouncing error can make the user feel scolded, especially if the error was a simple typo. A gentler approach is to use a small, brief motion, such as a subtle highlight that fades in, or a field that nudges slightly, combined with clear text explaining what is wrong and how to fix it. The motion's job is to guide attention, not to punish, and the text's job is to explain, because motion alone cannot convey what the error is or how to correct it.

Success motion also deserves restraint. A celebration animation, such as confetti or a large checkmark, can be delightful for a significant achievement, such as completing a course or making a first purchase, but it can be annoying for routine actions, such as saving a setting or sending a message, where a brief confirmation is sufficient. Reserve large celebrations for genuinely significant moments, and use small confirmations, such as a button changing to a checkmark briefly, or a toast message fading in, for routine successes. That hierarchy of celebration matches the user's emotional experience: routine actions should feel efficient, significant actions should feel rewarding, and confusing the two makes routine actions feel slow and significant actions feel ordinary.

Timing for feedback motion is critical. Feedback should appear immediately after the action, within a tenth of a second if possible, because a delayed confirmation leaves the user uncertain whether their action registered. If the action's result takes time, such as a server request, show an intermediate state promptly, such as a loading button, and then show success or error when the result arrives, rather than leaving the user waiting in silence. That sequence, immediate acknowledgement, then outcome, is the basic pattern for trustworthy feedback, and motion can enhance each stage, from the button's press feedback to the result's entrance, without delaying either stage.

Finally, feedback motion should be consistent and predictable. If success always fades in from the top, and errors always highlight in place, users will learn those patterns and recognise them quickly, even peripherally. Inconsistent feedback, where success sometimes slides, sometimes pops, and sometimes does nothing, forces users to re-interpret each time, which slows them down and erodes trust. Define feedback patterns in your design system, use them consistently, and test them with real actions, including failures, because error feedback is often the least tested and most needed motion in an interface.

Check: Why should error motion be paired with text rather than used alone?

Section 26 — Modal and dialog animation

Modals and dialogs are overlays that demand attention, and their animation should help the user understand that a new layer has appeared, without disorienting them. A typical modal animation has two parts: the backdrop fades in, dimming the page behind it, and the panel itself fades and scales or slides in, drawing attention to its centre. The backdrop fade signals that the underlying page is temporarily inactive, while the panel motion signals where the new content is and where it came from. Together, they create a clear layering: the page recedes, the modal advances, and the user's focus should follow the panel.

The panel's entrance should be modest. A scale from ninety-five percent to one hundred percent, combined with a fade, is a common and effective choice, because it suggests emergence without a large movement that distracts from reading the modal's content. A slide from the top or bottom can also work, especially for sheets on mobile, where sliding from the bottom mirrors the platform's sheet pattern. What matters is consistency: if modals in your product always scale in, users will learn that pattern and recognise a modal instantly, whereas if each modal animates differently, the pattern is lost.

Exit animation for a modal is often neglected, but it matters for orientation. When a modal closes, it should fade and scale out, or slide out in the reverse direction of its entrance, rather than vanishing instantly, so the user sees it leave and understands they are returning to the underlying page. That exit animation requires keeping the modal mounted during the exit, as with page transitions, and removing it only after the animation completes. If the modal vanishes instantly, the user may be momentarily disoriented, especially if the underlying page has changed while the modal was open, because there is no motion to bridge the two states.

Focus management is inseparable from modal animation. When a modal opens, focus should move to the modal, typically to its first interactive element or its heading, and it should be trapped within the modal until it closes, so keyboard users cannot tab to the inactive page behind it. When the modal closes, focus should return to the element that opened it, so the user does not lose their place. Animation must not interfere with that focus flow: focus should move promptly, even if the entrance animation is still playing, because a user waiting for focus to move is a user who cannot act. Test modal animation with a keyboard, and confirm focus moves correctly in both directions, regardless of animation timing.

Finally, modals must respect reduced motion, by appearing and disappearing without scale or slide when that setting is active, using a simple fade or no animation. They must also be dismissible by keyboard, typically with the Escape key, and by clicking the backdrop where appropriate, with those dismissals triggering the same exit animation and focus return as a close button. A modal animation that looks polished but strands focus, or that cannot be dismissed by keyboard, is not a finished modal; it is a trap. Animation should enhance the modal's layering, not compromise its usability.

Check: Why must focus move promptly when a modal opens, even if its animation is still playing?

Section 27 — Accordion and collapsible animation

Accordions and collapsible sections expand and collapse to reveal or hide content, and animating that change helps users track what is happening. Without animation, content appears or disappears instantly, and the user may not notice that other content has shifted down or up to accommodate it. With animation, the section grows or shrinks gradually, and surrounding content moves smoothly, so the user can follow the layout change and maintain their place on the page. That tracking is especially important for long pages, where an instant expansion can push content the user was reading far down the page without warning.

The technical challenge is height animation, because the expanded height is often unknown, as it depends on content that varies. CSS cannot transition from height auto to a fixed height, because auto is not a number, so a common workaround is to animate max-height from zero to a large value that exceeds the content's height. That workaround works, but it has flaws: the animation duration is based on the max-height value, not the actual content height, so short content animates slowly, as if it had a large height, and long content may be clipped if the max-height is too small.

A newer CSS feature, grid-template-rows animation, offers a cleaner solution for some cases. By placing content in a grid row that transitions from zero fractional units to one fractional unit, with overflow hidden, the content can expand smoothly without measuring, because the grid tracks the content's size. That technique is elegant and requires no JavaScript measurement, but it depends on browser support and on understanding grid layout, so test it in your target browsers before relying on it. Whichever technique you use, the principle is the same: the animation should reflect the actual content height, not an arbitrary large value, so short and long sections animate at appropriate speeds.

Content inside an expanding section should also be considered. If the section contains images, forms, or interactive elements, those should become focusable only when the section is expanded, and should not be reachable by keyboard when collapsed, because tabbing to hidden content confuses keyboard users. That can be achieved by hiding collapsed content with visibility or by managing focusability, in addition to animating height. Also, the accordion's trigger, typically a button, should indicate its state with an icon that rotates, such as a chevron, and with an expanded attribute for assistive technology, so the state is clear both visually and programmatically. The chevron rotation is a small motion that complements the expansion, signalling direction: down when collapsed, up when expanded, or rotating smoothly between them.

Finally, keep accordion animations short, around two to three tenths of a second, because a slow expansion delays access to content the user explicitly requested, which is frustrating. If an accordion contains a lot of content, a slightly longer duration may be justified, but it should still feel responsive, not ceremonial. Test accordions with content of varying lengths, with keyboard navigation, and with screen readers, confirming that the expanded state is announced, that focus remains sensible, and that the animation does not clip or jump. An accordion that animates height correctly, manages focus correctly, and signals state clearly is a small but complete example of motion design done well.

Check: Why does animating max-height to a large value make short accordion content animate slowly?

Section 28 — Carousel and slider motion

Carousels and sliders show one item at a time from a set, with controls to move forwards, backwards, or to a specific item, and their motion should help users understand the set's structure and their position within it. The most common motion is a horizontal slide, where the current item slides out and the next slides in from the appropriate direction: forwards from the right, backwards from the left, mirroring reading direction in left-to-right languages. That directional consistency is important, because it lets users infer direction from motion: if items always slide from the right when moving forwards, the user learns that rightward motion means forward, and can predict the next motion before it happens.

An alternative is a fade, where items cross-fade without sliding, which is gentler and works well when items are unrelated or when sliding would imply an order that does not exist. Fades are also easier to implement and less likely to cause layout issues, because they do not require moving a track of items. However, fades give less directional information, so they are less suitable for carousels where order and direction matter, such as a product gallery where the user is browsing sequentially.

Implementation for a sliding carousel typically involves a track element containing all items in a row, with the track translated by transform to show the current item. Animating the track's transform, rather than each item individually, is efficient and gives a smooth slide, because the compositor can move the track as a layer. The track's width must accommodate all items, and overflow hidden on the container clips the non-current items. For infinite carousels, where moving past the last item wraps to the first, the implementation must handle the wrap without a visible jump, often by cloning items or by resetting the track position without animation after the slide completes. That wrap handling is the main complexity in carousel code, and it should be tested carefully, because a visible jump at the wrap breaks the illusion of a continuous set.

Auto-playing carousels deserve caution. A carousel that advances automatically can distract readers, can move content they were reading, and can be inaccessible if it cannot be paused. If auto-play is used, it should be slow, typically several seconds per item, it should pause on hover and on focus, and it should provide a clear pause control, because users should be able to stop motion that interferes with reading. Many accessibility guidelines recommend against auto-playing carousels, or require a pause mechanism, and for most content, a user-controlled carousel is better than an auto-playing one, because it respects the reader's pace rather than imposing a pace on them.

Finally, carousel motion must be paired with clear position indicators, such as dots, numbers, or thumbnails, that show how many items there are and which is current, and with controls that are large enough to tap and reachable by keyboard. Motion alone does not tell the user where they are in the set; indicators do. Test carousels with keyboard navigation, with screen readers, and on touch devices, where swipe gestures should complement, not replace, button controls, because not all touch users can swipe precisely. A carousel whose motion, indicators, and controls work together is a navigable set, not just a slideshow.

Check: Why should a carousel slide from the right when moving forwards in a left-to-right language?

Section 29 — Navigation and menu motion

Navigation menus, especially on mobile, use motion to show hierarchy and to help users understand where they are. A mobile menu that slides in from the side, for example, signals that it is a layer over the page, and that closing it will return to the page, whereas a menu that appears instantly gives no such signal. A dropdown menu that fades and slides down slightly signals that it belongs to its trigger, and that it will disappear when the trigger is activated again or when focus moves away. Those motions are not decorative; they encode the menu's relationship to the page and to its trigger, which helps users build a mental model of the site's structure.

Drawer menus, which slide in from an edge, are the most common mobile navigation pattern. Their animation should be fast, around two to three tenths of a second, and should use transform translation, so it is smooth and does not trigger layout. The drawer should also include a backdrop fade, as with modals, to signal that the page behind is inactive, and it should trap focus while open, returning focus to its trigger when closed.

Dropdown and mega menus on desktop have their own motion considerations. A dropdown that appears instantly on hover can feel abrupt, and one that appears after a delay can feel unresponsive, so a short fade and slight slide, around one to two tenths of a second, is a good balance. Dropdowns should also handle hover intent: moving the pointer diagonally from a trigger to a submenu should not close the menu prematurely, which requires either a delay before closing or a hover-intent algorithm that predicts the pointer's path. Those details are not strictly animation, but they affect how motion feels, because a menu that flickers open and closed as the pointer moves feels broken, however smooth its individual animations are.

Menu motion should also respect the user's navigation history. A menu item that is currently active should be indicated clearly, not only by colour, but by a persistent indicator, such as an underline or a background, that does not rely on motion to be understood. When navigating to a new page, the menu's active indicator should move or change, and if it animates, it should do so briefly, guiding the eye to the new active item without delaying navigation. That indicator motion is a micro-interaction, similar to a tab indicator, and it helps users confirm they are where they intended to be, which is especially valuable in large sites with many sections.

Finally, test navigation motion with all input methods and with reduced motion. A drawer that slides in beautifully with a mouse may be unusable with a keyboard if focus is not trapped, and a dropdown that relies on hover will be unusable on touch if it has no tap handling. Reduced-motion users should get menus that appear instantly, without slide or fade, but with the same focus management and state clarity. Navigation is the skeleton of a site, and its motion should make that skeleton clearer, not more elaborate, because users navigate to reach content, not to admire the navigation itself.

Check: Why should a mobile drawer menu include a backdrop fade as well as a slide?

Section 30 — Choreography and hierarchy

Choreography in motion design is the coordination of multiple animations so they work together as a coherent sequence, rather than competing for attention. When a page loads, a modal opens, or a view changes, several elements may animate at once: a backdrop fades, a panel slides, content fades in, and a button pulses. If all of those start at the same instant with the same duration, they compete, and the user does not know where to look. Choreography orders them: the backdrop fades first, establishing the layer, then the panel appears, then its content, each step building on the previous one, so attention is guided in a deliberate order, like a story with a beginning, middle, and end.

Hierarchy is the principle behind choreography: more important elements should animate first, or more prominently, while less important elements animate later, or more subtly. A modal's heading and primary action are more important than its decorative illustration, so they should appear earlier or with more emphasis. A list's first item is more important than its tenth, so stagger should reflect that order, not randomise it. That hierarchy mirrors visual hierarchy in static design, where size, colour, and position signal importance, but in motion, timing and prominence signal importance as well. A well-choreographed sequence therefore reinforces the page's information hierarchy, rather than undermining it with simultaneous motion.

A practical choreography pattern is to define stages: stage one establishes context, such as a backdrop or a container; stage two introduces the primary content, such as a panel or a heading; stage three introduces secondary content, such as details or actions. Each stage should overlap slightly with the previous one, rather than waiting for it to finish completely, because a fully sequential choreography can feel slow, while a slightly overlapping one feels fluid. The overlap should be small, such as starting stage two when stage one is halfway complete, so the sequence feels connected rather than disjointed. That overlap is a matter of tuning: too much overlap returns to competition, too little feels like waiting.

Choreography also applies to exits and to interruptions. When a view closes, its elements should exit in reverse order of their entrance, or in an order that makes sense for departure, such as content fading first, then the panel, then the backdrop, so the layer dissolves in reverse. If a choreography is interrupted, for example by a user dismissing a modal mid-entrance, the exit choreography should start from the current state, not from the entrance's final state, or the exit will jump. That requires animations that can be interrupted and retargeted, which is easier with JavaScript animations than with CSS alone, but it is achievable in CSS with careful class management and fill modes.

Finally, choreography should be used sparingly. Not every page load or view change needs a multi-stage choreography; many need only a simple fade or slide. Choreography is most valuable for significant moments, such as onboarding, first-run experiences, or major view changes, where guiding attention through a sequence genuinely helps the user understand a new interface. For routine interactions, simple, fast motion is better, because choreography adds time and complexity that routine tasks do not need. Reserve choreography for moments where the sequence itself teaches something, and keep routine motion simple and consistent.

Check: Why should choreography stages overlap slightly rather than run fully sequentially?

Section 31 — Duration guidelines

Duration is the most consequential choice in motion design, because it determines whether motion feels responsive or sluggish, regardless of how elegant its easing or path may be. As a starting point, most interface animations should last between two tenths and five tenths of a second. Micro-interactions, such as button feedback and hover effects, should be at the short end, around one to two tenths, because they respond to direct input and the user expects immediate feedback. View transitions, such as page changes and modal entrances, can be at the longer end, around three to four tenths, because they involve larger changes and need time to be perceived. Entrances for complex views may reach five tenths, but beyond that, motion starts to feel like waiting rather than guiding.

Those ranges are guidelines, not rules, and they should be adjusted for the size of the movement and the importance of the content. A small fade can be shorter than a large slide, because the eye needs less time to follow a small change.

Duration also interacts with user expectation and with frequency. An animation that plays rarely, such as a first-run onboarding, can be longer, because the user is not in a hurry and the motion itself is part of the experience. An animation that plays frequently, such as switching tabs or hovering over list items, must be short, because its total time across many repetitions adds up, and a user who switches tabs twenty times will quickly tire of a half-second transition each time. That frequency consideration is why tab switches and list hovers should be among the shortest animations in a product, even though they are small, because their cost is paid repeatedly.

A common mistake is to make durations too long in an effort to showcase motion, especially for entrances and for decorative effects. A one-second fade-in for a page's main content, for example, delays the user's ability to read that content by a second, which is a significant cost for a purely aesthetic effect. Another mistake is to make durations inconsistent, so similar actions take different times, which makes the interface feel unpredictable. Define duration tokens, such as fast, standard, and slow, with values within the guideline range, and use them consistently for similar motions, so users learn how long things take and can predict them, which contributes to a feeling of responsiveness even when durations are not minimal.

Finally, durations should adapt to reduced motion and to user preferences. When reduced motion is active, durations should become effectively zero, with content appearing immediately, because the user's preference is for less motion, not for slower motion. When a user is on a slow device or a slow connection, durations should not compound delays: an animation that plays while content is still loading can make the wait feel longer, so consider delaying or shortening animations until content is ready, rather than animating placeholders for a long time. Duration is a design decision that balances perception, responsiveness, and respect for the user's time, and it deserves deliberate choice for every animation.

Check: Why should frequently repeated animations be shorter than rare ones?

Section 32 — Delays and sequencing in practice

Delay is the pause before an animation starts, and it is a powerful tool for sequencing, but it is also easy to misuse, because a delay is, from the user's perspective, waiting. A short delay, such as fifty milliseconds for a staggered list item, is perceived as rhythm, not as waiting, because it is part of a sequence the user can follow. A long delay, such as a second before a modal appears, is perceived as slowness or unresponsiveness, because the user has already acted and is waiting for a response. The distinction is not merely about length; it is about whether the delay is part of a visible sequence, or whether it is a silent gap before any feedback appears.

A practical rule is to never delay initial feedback. When a user clicks, taps, or presses a key, some feedback should appear immediately, within a tenth of a second, even if the full result takes longer. That feedback can be a button state change, a loading indicator, or a partial animation, but it must not be delayed, because delayed feedback makes the user doubt whether their action registered.

Negative delays are a useful technique for animations that should start partway through, such as a looping animation that should not start from its beginning when it first appears, or a staggered animation where later items should already be in motion when they become visible. A negative delay of minus two hundred milliseconds on a four-hundred-millisecond animation means the animation starts at its halfway point, as if it had been running for two hundred milliseconds already. That can make a group of animations feel already alive when they appear, rather than starting stiffly from rest, but it should be used carefully, because starting mid-animation can confuse if the starting state is not what the user expects.

Delays in transitions and animations also interact with user interruption. If a user triggers an action, then triggers another action before the first animation's delay has elapsed, the first animation may start after the second action, producing a confusing out-of-order effect. To handle that, delays should be cancellable: when a new action supersedes an old one, pending delayed animations should be cancelled, not allowed to play late. That requires managing timers or animation objects explicitly, rather than relying on CSS delays alone, which are harder to cancel cleanly. In CSS, removing a class cancels its transition, but a delayed transition that has not yet started may still need careful handling to avoid a late appearance.

Finally, test delays with real interaction speeds, not just with slow, deliberate clicks. A delay that feels like rhythm when you click once and watch may feel like lag when you click rapidly, navigate quickly, or use a keyboard to move through many items. If delays cause animations to queue up and play long after the user has moved on, shorten or remove them, because a sequence that lags behind the user's actions is worse than no sequence at all. Delays should create order, not backlog, and that distinction is revealed only by testing at real interaction speeds.

Check: Why should initial feedback never be delayed, even in a sequenced animation?

Section 33 — Spring physics and natural motion

Spring physics offers an alternative to duration-based easing, by modelling motion as a physical spring with stiffness, damping, and mass. Instead of specifying how long an animation takes, you specify how stiff the spring is, how much damping resists its movement, and how heavy the object is, and the animation's duration emerges from those properties and from the distance it must travel. A stiff spring with low damping will overshoot its target and oscillate, giving a bouncy, playful feel. A soft spring with high damping will approach its target slowly and smoothly, without overshoot, giving a gentle, weighty feel. That physical model can produce motion that feels more natural than a fixed easing curve, because it mimics how real objects move.

Springs are particularly useful for interactive motion, where the target may change while the animation is running, such as dragging an element and releasing it, or a panel that follows a gesture and then settles. With a duration-based animation, retargeting mid-animation requires restarting the animation from its current value with a new duration, which can feel abrupt.

Implementing springs in the browser usually involves a library or a custom physics loop, because CSS does not natively support spring easing, although newer CSS features are beginning to offer spring-like curves. A custom spring loop uses requestAnimationFrame, and on each frame, computes acceleration from the spring's displacement and velocity, updates velocity and position, and sets the element's transform. The maths is not complex, but it requires care with stability: a spring with too little damping or too large a time step can oscillate wildly or diverge, rather than settling. Using a well-tested library or a known-stable integration method is safer than inventing spring maths from scratch, especially for production interfaces.

Choosing spring parameters is a matter of feel, similar to choosing easing, but with different intuitions. Stiffness controls speed: higher stiffness means faster movement and more overshoot. Damping controls oscillation: higher damping means less overshoot and a quicker settle, but also a slower feel if damping is too high. Mass controls inertia: heavier objects move more slowly and feel weightier. A good starting point for interface springs is moderate stiffness, damping that is slightly underdamped, giving a tiny overshoot that feels lively without looking like a bounce, and low mass. Test springs on the actual elements they will animate, because a spring that feels right for a small icon may feel wrong for a large panel, whose greater visual weight demands a gentler spring.

Finally, springs should respect the same principles as other motion: keep them short enough to feel responsive, respect reduced motion by using a simple fade or instant change when that setting is active, and do not use bouncy springs for serious or frequent actions, where overshoot becomes tiresome. Springs are a tool for natural, interactive motion, not a requirement for every animation, and a well-chosen easing curve is often simpler and more predictable for routine transitions. Use springs where their physical continuity adds value, such as gestures and retargetable motion, and use easing where a fixed duration and a predictable end are more important.

Check: Why are springs well suited to animations whose target may change mid-animation?

Section 34 — Interpolation and easing in JavaScript

Interpolation is the process of computing intermediate values between a start and an end, and it is the core of JavaScript animation. For a numeric property, such as opacity or a pixel position, interpolation is simple: current equals start plus progress times the difference between end and start, where progress is a value between zero and one, derived from elapsed time and easing. For colours, interpolation means interpolating each channel, such as red, green, and blue, separately, which requires parsing colour strings into numbers, interpolating them, and reassembling them. For transforms, interpolation means interpolating each component, such as translate and scale, separately, which is why transform strings must be parsed or constructed from numeric values rather than interpolated as text.

Easing in JavaScript is a function that takes a linear progress value, between zero and one, and returns an eased progress value, also between zero and one, but distributed differently over time. A linear easing returns its input unchanged. An ease-out function returns a value that rises quickly and then flattens, such as one minus the square of one minus progress. An ease-in-out function rises slowly, then quickly, then slowly.

A practical JavaScript animation function therefore has a clear structure: it takes an element, a set of start and end values, a duration, and an easing function, and it runs a requestAnimationFrame loop that computes elapsed, progress, eased progress, and current values, applies them to the element, and stops when progress reaches one. That function can be reused for many animations, with different values and easings, and it can be extended to support cancellation, by storing the frame request and providing a cancel method, and to support promises, by resolving a promise when the animation completes. Building that reusable function once, and using it consistently, is better than writing a new loop for each animation, because it centralises time handling, easing, and cleanup.

Common mistakes in JavaScript interpolation include interpolating non-numeric values as if they were numbers, such as trying to interpolate between auto and a pixel value, which fails because auto is not a number, and forgetting to clamp progress between zero and one, which can cause values to overshoot or undershoot after the animation should have ended. Another mistake is to read layout properties inside the interpolation loop, such as offsetWidth, which forces synchronous layout and slows the animation. To avoid that, measure layout before the animation starts, store the values, and interpolate from stored numbers, rather than measuring per frame. Those habits keep JavaScript animation both correct and performant.

Finally, JavaScript interpolation should be used where it adds value over CSS, such as when values are computed at runtime, when multiple properties must be coordinated precisely, or when animations must be interrupted and retargeted smoothly. For simple, static animations, CSS is usually clearer and less code, because the browser handles timing and interpolation for you. JavaScript interpolation is a powerful tool, but it is not a replacement for CSS; it is a complement for dynamic cases, and choosing between them based on whether values are known statically or computed dynamically is a reliable guide.

Check: What does an easing function do to a linear progress value?

Section 35 — Gesture-driven motion

Gesture-driven motion is motion that follows the user's direct manipulation, such as dragging, swiping, or pinching, rather than playing a predetermined animation after an action. When a user drags a card, the card should follow their finger or pointer exactly, with no lag and no easing that separates it from their input, because the card is, in the user's mind, held by their finger. When they release it, the card should then animate to its final state, such as snapping back, snapping to a new position, or dismissing, with a motion that reflects the gesture's velocity and direction. That two-phase pattern, direct following during the gesture, then animated settling after release, is the core of gesture-driven design.

Implementing gesture following requires handling pointer events, which unify mouse, touch, and pen input in modern browsers. On pointerdown, record the starting position and capture the pointer, so movement continues to be tracked even if the pointer leaves the element. On pointermove, compute the delta from the start, and set the element's transform to follow that delta, using transform translation for performance.

Thresholds and feedback during a gesture help users understand what will happen on release. As a card is dragged towards dismissal, it might fade or scale slightly, signalling that it is approaching the dismissal threshold, and it might show a background action, such as a delete indicator, that becomes more prominent as the drag progresses. Those progressive signals let the user predict the outcome before releasing, and adjust their gesture if they did not intend it. Without them, the user must guess whether releasing will dismiss, move, or snap back, which makes gestures feel unpredictable and risky. Design gestures so their outcome is visible during the gesture, not only after it.

Gesture motion must also handle interruption and cancellation. If a gesture is cancelled, such as by the pointer leaving the screen or by a system interruption, the element should animate back to a sensible state, not remain halfway dragged. If a gesture conflicts with scrolling, such as a horizontal drag inside a vertically scrolling page, the implementation must decide which gesture takes precedence, often by waiting for a small movement threshold before locking to a direction, so vertical scrolling is not blocked by a slight horizontal wobble. Those edge cases are where gesture code most often fails, and they require testing on real touch devices, with real fingers, not only with a mouse, because mouse dragging and finger dragging feel different and have different precision.

Finally, gesture-driven motion must be accessible. Not all users can perform precise gestures, so every gesture action should also be available through a button, a keyboard action, or another non-gesture control. A card that can only be dismissed by swiping excludes users who cannot swipe, so provide a delete button as well. Gesture motion should enhance an interface for those who can use it, not gate functionality behind it, and its animated settling after release should respect reduced motion, by snapping instantly rather than animating, when that setting is active.

Check: Why should a dragged element follow the pointer exactly, without easing, during the drag?

Section 36 — The FLIP technique

FLIP is a technique for animating layout changes smoothly, by using transforms instead of animating layout properties directly. Its name stands for First, Last, Invert, Play, which describes its four steps. First, record the element's starting position and size, before the layout change. Last, apply the layout change, such as reordering a list or filtering items, and record the element's ending position and size. Invert, compute the difference between first and last, and apply a transform that moves and scales the element back to its first position, so it appears not to have moved, even though the layout has changed. Play, animate that transform to zero, so the element smoothly moves from its first position to its last position, using a fast transform animation rather than a slow layout animation.

FLIP is powerful because it solves a common problem: layout changes, such as sorting, filtering, or adding and removing items, are inherently layout operations, and animating them directly would require animating layout properties, which is slow. FLIP avoids that by letting the layout change happen instantly, then animating the visual difference with transforms, which are fast.

Implementing FLIP requires careful measurement. Before the change, use getBoundingClientRect to record each element's first position and size. After the change, record each element's last position and size, again with getBoundingClientRect. For each element that persists, compute the delta in x, y, width, and height, and apply a transform that inverts that delta: translate by the negative delta, and scale by the ratio of first size to last size, if size changed. Then, in the next frame, animate the transform to identity, using a transition or a Web Animations API call, so the element moves to its new position. For new elements, animate from opacity zero or a small scale; for removed elements, you must keep them in the document during their exit animation, then remove them afterwards, which requires cloning or delaying removal.

FLIP has pitfalls. Measuring layout with getBoundingClientRect can force synchronous layout if done carelessly, so measurements should be batched: record all first positions, apply the change, record all last positions, without interleaving reads and writes that force layout repeatedly. Scaling with FLIP can distort content, especially text, during the animation, because scaling an element scales its text as well, which can look blurry or stretched, although the final state is correct. To minimise distortion, FLIP is often used for position-only changes, with size changes handled separately or with a technique that counter-scales children.

Despite those pitfalls, FLIP is a valuable technique to understand, because it embodies a general principle: separate the layout change from the visual animation, and animate the difference with fast properties. That principle applies beyond FLIP, to any situation where a layout change needs to appear smooth. Libraries exist that implement FLIP for common cases, such as list reordering, and using a library is often safer than hand-rolling FLIP, because the measurement, batching, and cleanup details are easy to get wrong. Whether you use a library or hand-roll it, understanding First, Last, Invert, Play lets you reason about why the animation works and where it might fail.

Check: What do the four letters in FLIP stand for, and what does Invert do?

Section 37 — Skeletons versus spinners in detail

Skeletons and spinners were introduced earlier, but their choice deserves deeper treatment, because it affects perceived performance and user patience. A spinner signals only that work is happening, with no information about what will appear, how long it will take, or how much of the page is loading. A skeleton signals the shape of what will appear, preserving layout and giving the user a mental model of the eventual content, which reduces uncertainty and makes the wait feel shorter, even when the actual wait is identical. Research and practice consistently show that skeletons feel faster than spinners for content loading, because they reduce layout shift and give the user something to anticipate, rather than a generic rotating indicator.

Skeletons are not always better. For a small, local action, such as saving a form or loading a single value, a spinner or a button loading state is more appropriate, because a skeleton for a tiny area would be disproportionate and confusing. For a full page or a large section, a skeleton is usually better, because it preserves the page's structure.

Designing a good skeleton requires mirroring the real layout accurately, but not too accurately. A skeleton should suggest the structure, such as lines for text, blocks for images, and circles for avatars, without trying to replicate the exact content, because a skeleton that looks too much like real content can be mistaken for it, especially in screenshots or for users who glance quickly. Skeleton shapes should be low-contrast, neutral grey, with rounded corners that match the real content's shapes, and they should not contain text, because skeleton text would be mistaken for real text. The shimmer animation, if used, should be subtle and slow, with a low-contrast gradient moving across the skeleton, and it should be disabled under reduced motion, leaving static grey shapes.

A common skeleton mistake is to show skeletons for too long, or for content that loads quickly, causing a flash of skeleton that feels like a glitch. As with spinners, a threshold can help: only show a skeleton if loading exceeds a brief period, such as two hundred milliseconds, so fast loads show content directly without a skeleton flash. Another mistake is to show skeletons that do not match the eventual layout, causing a layout shift when content arrives, which defeats the skeleton's main benefit. To avoid that, skeletons should be built from the same layout components as the real content, with the same sizes and spacing, differing only in that their content is grey shapes rather than real data.

Finally, skeletons must be accessible. A skeleton should not be announced as content by screen readers, because it is not content; it should be hidden from assistive technology, with a single loading announcement, such as "Loading", that is polite and not repeated. When content arrives, it should be announced or made available in a sensible order, and focus should not be trapped in the skeleton area. A skeleton that is visually polished but inaccessible, or that causes layout shift, or that flashes for fast loads, has failed its purpose, which is to make waiting feel shorter and less disruptive, not to add a new kind of disruption.

Check: Why can a skeleton feel faster than a spinner even when the wait time is identical?

Section 38 — Progress indicators

Progress indicators show how far a process has advanced, and they come in several forms, each suited to a different kind of progress. A determinate progress bar shows a proportion complete, from zero to one hundred percent, and is appropriate when progress can be measured, such as a file upload, a multi-step form, or a data import with known total work. An indeterminate progress bar, which animates back and forth without showing a proportion, is appropriate when progress cannot be measured, but work is known to be happening, such as a server request of unknown duration. A stepper, which shows a series of steps with current, completed, and upcoming states, is appropriate for a multi-step process where the steps themselves are meaningful, such as checkout or onboarding, because it shows both progress and structure.

Determinate progress should be honest. If a progress bar jumps from ten percent to ninety percent instantly, or stalls at ninety-nine percent for a long time, users will distrust it, because it does not reflect actual progress.

Steppers deserve special attention, because they combine progress with navigation and with information about what remains. A stepper should show step labels clearly, indicate the current step with a strong visual signal, such as a filled circle or a highlighted label, and indicate completed steps with a checkmark or a muted filled state. It should also allow navigation back to previous steps where appropriate, because users may need to correct an earlier step, and it should not imply that future steps are already complete. Animating a stepper, such as sliding a progress line forward or animating a checkmark when a step completes, can reinforce progress, but the animation should be brief and should not delay navigation to the next step, because the user is trying to complete a process, not watch a progress animation.

Motion in progress indicators should be purposeful and not distracting. An indeterminate bar's animation should be smooth and continuous, with a speed that suggests activity without urgency, because a frantic indeterminate animation can make a short wait feel stressful. A determinate bar's fill animation should be smooth, with transitions on its width or transform, so updates do not jump abruptly, but it should also reflect real progress promptly, not lag behind it due to a slow transition. For steppers, the transition between steps should be brief, and the new step's content should become available promptly, because the progress indicator is a guide to the process, not the process itself.

Accessibility for progress indicators includes labelling them with their purpose and, for determinate indicators, their current value, so screen readers can announce progress. The HTML progress element provides native semantics for determinate and indeterminate progress, and should be used where appropriate, rather than a div styled as a bar, because its semantics are built in. For steppers, use a list with current step indicated programmatically, so assistive technology can convey the structure. Progress indicators that are only visual, with no text or semantics, exclude readers who cannot see them, even though progress is information all users need.

Check: When should you use an indeterminate indicator instead of a determinate one?

Section 39 — Celebration and delight, with restraint

Celebration animations, such as confetti, fireworks, or a large animated checkmark, can make significant moments feel rewarding, but they must be used with restraint, because their impact depends on rarity. If every routine action triggers confetti, confetti becomes noise, and it loses its ability to mark a genuinely significant achievement. Worse, frequent celebrations slow down routine tasks, because the user must wait for or dismiss the celebration before continuing, which turns a delightful moment into an interruption. Reserve large celebrations for moments that are genuinely significant to the user, such as completing a course, reaching a milestone, or making a first purchase, and use small confirmations for routine actions, as earlier sections advise.

Delight can also come from smaller, subtler motions, such as a well-timed micro-interaction, a playful icon animation, or a transition that feels weighty and polished, rather than from large celebrations. Those small delights accumulate into a feeling that the product is crafted and enjoyable, without interrupting tasks or drawing excessive attention. A button that responds with a satisfying press, a list that staggers in gracefully, or a toggle that slides smoothly can all be delightful, because they make ordinary interactions feel good, even though none of them is a celebration in itself. That everyday delight is more valuable than occasional confetti, because it is experienced constantly, not rarely.

Cultural and personal differences matter for celebration. What feels delightful to one user may feel childish, distracting, or even inappropriate to another, especially in professional or serious contexts, such as finance, health, or work tools, where a confetti burst for a routine task can feel tone-deaf. Consider the context: a learning app celebrating a completed lesson with confetti may be appropriate, while a banking app celebrating a payment with confetti may feel frivolous and may undermine trust. When in doubt, choose a smaller, more neutral confirmation, such as a checkmark and a message, over a large celebration, because a neutral confirmation is rarely wrong, while a large celebration can be wrong in many contexts.

Technical implementation of celebrations should also respect performance and accessibility. Confetti, which may involve many particles animated on canvas, can be expensive and can cause jank if not optimised, so it should be limited in particle count, duration, and area, and it should not run indefinitely. It should respect reduced motion, by showing a static confirmation instead of animated confetti when that setting is active, and it should not interfere with focus or with the user's ability to continue, by providing a clear way to dismiss it or by auto-dismissing it promptly. A celebration that cannot be dismissed, that blocks the interface, or that runs forever is not delightful; it is an obstacle.

Finally, test celebrations with real users and in real contexts, because their reception is subjective and context-dependent in ways that guidelines cannot fully predict. What looks delightful in a demo may feel annoying in daily use, and what feels delightful for a first achievement may feel tedious for the tenth. Use celebrations sparingly, measure their reception where possible, and be willing to remove or soften them if they do not land. Delight is a seasoning, not a main course, and like any seasoning, it works best in small amounts, in the right dish, at the right moment.

Check: Why does frequent celebration reduce the impact of a genuinely significant celebration?

Section 40 — Motion design tokens

Motion design tokens are named values for motion properties, such as duration, easing, and delay, that are shared across a product, so motion feels consistent and can be changed in one place. Just as colour tokens name colours and spacing tokens name distances, motion tokens name durations, such as duration-fast, duration-standard, and duration-slow, easings, such as ease-standard, ease-entrance, and ease-exit, and delays, such as delay-stagger, that are used by all components. That system prevents the inconsistency where one component uses a two-tenths duration with ease-out, another uses three tenths with ease-in-out, and a third uses a custom curve, all for similar motions, which makes the product feel assembled from unrelated parts.

Tokens should be few and purposeful. A typical set might include three durations: fast for micro-interactions, around one to two tenths of a second; standard for most transitions, around two to three tenths; and slow for larger view transitions, around four to five tenths. It might include three easings: standard for most motion, entrance for elements appearing, and exit for elements leaving, each a cubic-bezier curve chosen for its feel. It might include a stagger delay, such as fifty milliseconds, for sequenced lists.

Implementing tokens in CSS can be done with custom properties, such as --duration-fast, --ease-standard, and --delay-stagger, defined on a root element and used in transition and animation declarations throughout the product. That approach makes tokens easy to change globally: updating one custom property updates every animation that uses it, which is valuable when tuning motion after user feedback. It also makes tokens visible in code, so a developer reading a transition can see that it uses the standard duration and easing, rather than a magic number, which aids maintenance and review. For JavaScript animations, tokens can be shared as constants or as CSS variables read at runtime, so CSS and JavaScript motion use the same values.

Tokens also support theming and accessibility. A reduced-motion theme can override motion tokens to effectively disable animation, by setting durations to near zero and iterations to one, which is cleaner than overriding every animation individually. A theme for a different brand or product can change motion tokens to give it a different feel, such as faster, snappier motion for a playful product, or slower, gentler motion for a calm product, without changing component code. That separation of motion values from component logic is the same principle as colour tokens: values change in one place, components use names, and the product's feel can evolve without rewriting components.

Finally, document motion tokens with examples, not just values. A token document should show each duration and easing in action, with a small demo, so designers and developers can see and feel the difference, rather than guessing from numbers. It should also state when each token should be used, such as fast for feedback, standard for transitions, and slow for view changes, so choices are guided, not arbitrary. Motion tokens are a small investment that pays off in consistency, maintainability, and the feeling that a product's motion belongs together, which is the hallmark of a mature design system, a subject the next chapter explores in full.

Check: Why are motion tokens better than hard-coding durations in each component?

Section 41 — Testing motion

Testing motion requires checking both that animations run correctly and that they feel right, because an animation can be technically correct and still feel wrong, or feel smooth but end in the wrong state. Automated tests can check the technical side: that an animation starts when expected, that it ends at the correct values, that it is cancelled cleanly when interrupted, and that it respects reduced motion. Those tests can be written by triggering an animation, advancing time in a test environment, and asserting on computed styles or on element properties, which catches regressions where an animation's duration, easing, or end state changes unintentionally. Automated tests are less good at judging feel, which requires human observation, so they should complement, not replace, manual testing.

Manual testing for motion should be done at real speed, on real devices, with real interaction, because slow-motion or desktop-only testing hides problems that appear in normal use. Test each animation by triggering it repeatedly, by triggering it quickly in succession, by interrupting it mid-way, and by triggering it while other animations are running, because those are the conditions under which motion most often breaks. Check that interrupted animations do not leave elements in intermediate states, that rapid triggers do not stack animations or cause flicker, and that simultaneous animations do not compete in a way that distracts from the primary action.

Visual testing for motion can use screen recordings or screenshot sequences, comparing an animation's frames against an expected sequence, but that approach is brittle, because small timing differences can cause false failures. A more practical visual test is to record an animation and review it by eye, checking for smoothness, for correct start and end states, for appropriate duration, and for any visual glitches, such as clipping, overlap, or text blurring during the animation. That review should be done for each animation at least once, and again when its code changes, because motion regressions are often visual and are missed by automated tests that only check end states.

Performance testing for motion, using the browser's performance tools, should check frame times, layout and paint activity, and processor usage, especially on a phone or with throttled settings. An animation that is smooth on a desktop may be janky on a phone, and an animation that is smooth in isolation may be janky when combined with other work, such as data loading or rendering a large list. Test animations in context, with realistic data and realistic device constraints, not in isolation on a powerful machine, because context is where performance problems appear. If an animation cannot run smoothly in context, simplify it, as earlier sections advise, rather than shipping a janky effect.

Finally, test motion accessibility, by enabling reduced motion and confirming that animations are disabled or shortened, that content remains available, and that feedback remains clear. Test with a keyboard, confirming that focus moves correctly during and after animations, and with a screen reader, confirming that animated content is announced appropriately and that motion does not hide information. Motion testing is therefore a combination of automated correctness tests, manual feel tests, performance tests, and accessibility tests, each catching a different class of problem, and together ensuring that motion serves users rather than merely impressing them.

Check: Why are automated tests alone not enough to judge motion quality?

Section 42 — Debugging jank

Jank is the visible stutter or jump in an animation, and debugging it starts with identifying whether it is caused by JavaScript, layout, paint, or compositing, because each cause has a different fix. The browser's performance tools are the primary diagnostic: record an animation, look at the frame timeline, and see which stages take long, whether frames are dropped, and whether long tasks block the main thread. A long JavaScript task during an animation, such as data processing or layout measurement, will show as a long task that delays frames. A layout-heavy animation will show as repeated layout work on every frame. A paint-heavy animation will show as large paint areas. Identifying the stage is the first step, because optimising the wrong stage wastes effort.

A common cause of jank is layout thrashing, where JavaScript alternately reads layout properties, such as offsetWidth or getBoundingClientRect, and writes styles, forcing the browser to recalculate layout synchronously on each read, rather than batching it. That pattern is slow because layout is recalculated many times per frame, instead of once. The fix is to batch reads together, then batch writes together, so layout is calculated once, or to avoid layout reads in the animation loop altogether, by measuring before the animation and using stored values.

Another common cause is animating expensive properties, such as box-shadow, filter, or background gradients, which require repainting on every frame, especially for large elements. The fix is to replace those animations with transform or opacity equivalents where possible, such as animating the opacity of a shadow pseudo-element rather than the shadow itself, or to reduce the animated area, such as animating a small element rather than a full-screen background. Large images, complex gradients, and backdrop filters are particularly expensive, and animating them should be avoided or minimised, because their paint cost can exceed the frame budget even on fast devices.

A third cause is work competing with animation, such as network requests, data processing, or other animations running simultaneously. If a frame's JavaScript includes both animation updates and heavy data work, the animation will starve. The fix is to move heavy work off the main thread where possible, such as using a web worker, or to defer it until after the animation completes, or to break it into smaller chunks that yield to the browser between frames. Similarly, multiple simultaneous animations that each trigger paint can combine to exceed the budget, even if each alone is fine, so test animations together, not just in isolation, and stagger or simplify them if they compete.

Finally, jank can be caused by the device or the browser itself, such as a low-power mode, a background process, or a browser extension, which means not all jank is fixable in code. Test on multiple devices and in a clean browser profile to distinguish code-caused jank from environment-caused jank, and focus on the former. If an animation is janky only on very low-end devices, consider simplifying it for those devices, or providing a reduced-motion or low-performance fallback, rather than removing it for all users. Debugging jank is a process of measurement, hypothesis, and verification, not guessing, and the performance tools are the evidence that guides it.

Check: What is layout thrashing, and why does it cause jank?

Section 43 — will-change and layer promotion

Will-change is a CSS property that hints to the browser that an element is about to change, so the browser can prepare, typically by promoting the element to its own compositing layer, which can make its animation smoother. For example, setting will-change: transform on an element that will soon animate its transform tells the browser to create a layer for it in advance, so the animation does not need to create the layer on its first frame, which could cause a hitch. That hint can improve the start of an animation, especially for elements that animate infrequently or for the first time, where layer creation cost would otherwise be paid during the animation itself.

However, will-change is a hint, not a guarantee, and it has costs that make it unsuitable as a blanket solution. Promoting many elements to layers consumes memory, especially on phones, because each layer is a bitmap that must be stored and composited. Too many layers can actually slow a page, by increasing compositing work and memory pressure, and can cause other problems, such as text blurring or rendering glitches on some devices.

A practical pattern is to add will-change dynamically, just before an animation starts, and remove it after the animation ends, using JavaScript or a class that is added and removed around the animation. For hover animations, that pattern is harder, because hover can start at any moment, so will-change on hoverable elements is sometimes set statically, but even then, it should be limited to a small number of elements, such as cards in a list, not to every element on the page. For elements that animate frequently, such as a carousel track, a static will-change may be justified, because the layer is used constantly, but for elements that animate rarely, dynamic will-change is better, because it pays the layer cost only when needed.

Layer promotion can also happen implicitly, through properties such as transform, opacity, or filter, which may cause the browser to create a layer automatically. That implicit promotion means will-change is often unnecessary, because the browser already promotes elements that are transformed or faded. The cases where will-change helps are those where the browser would not otherwise promote in advance, such as an element that is about to be transformed but is not yet transformed, or an element whose animation is triggered by a class change that the browser cannot predict. In those cases, a hint can smooth the first frame, but it should be tested: add will-change, measure, and confirm it actually improves smoothness, rather than assuming it does, because in many cases it makes no measurable difference.

Finally, will-change should not be used to fix a fundamentally slow animation, such as one that animates layout properties or large shadows, because layer promotion does not fix layout or paint costs; it only helps compositing. If an animation is janky due to layout or paint, the fix is to change the animated properties or to reduce paint work, not to add will-change. Will-change is a finishing touch for already-fast animations, not a cure for slow ones, and using it as a cure can hide the real problem while adding memory costs. Use it sparingly, measure its effect, and remove it when it is no longer needed.

Check: Why should will-change be removed after an animation completes?

Section 44 — Animating SVG paths and strokes

Animating SVG strokes, particularly the stroke-dashoffset technique introduced earlier, is a versatile tool for chart lines, illustrations, and icons, but it deserves detailed treatment because its details are easy to get wrong. The technique relies on stroke-dasharray and stroke-dashoffset, which control the pattern of dashes along a path. By setting dasharray to the path's total length, you create a single dash that covers the whole path, and by setting dashoffset to the same length, you shift that dash off the path, making it invisible. Animating dashoffset from the length to zero slides the dash onto the path, revealing it progressively, as if it were being drawn. That effect is simple in concept, but it requires knowing the path's length accurately, which is obtained with getTotalLength in JavaScript, or by setting dasharray to a large value that exceeds the length, which works approximately but can cause timing inaccuracies.

A related technique is animating a path's fill, such as a shape that fills with colour after its outline is drawn, by sequencing a stroke animation followed by a fill animation, using delays or keyframe percentages. That sequence, outline then fill, is a classic illustration entrance, and it works because stroke and fill are separate properties that can be animated independently, with the fill's opacity or colour animating after the stroke completes.

Stroke animation can also be used for progress indicators, such as a circular progress ring, where a circle's stroke-dasharray represents its circumference, and dashoffset represents progress. By animating dashoffset from the circumference to a value corresponding to the progress percentage, the ring fills progressively, which is a common and effective progress indicator for circular gauges. That technique is precise, because dasharray and dashoffset are numeric and can be computed from the radius, and it is performant, because it animates a stroke property that the browser can handle efficiently. It also works well with accessibility, provided the progress value is also available as text, because a visual ring alone does not convey its value to screen readers.

Common mistakes in stroke animation include using a dasharray that does not match the path length, causing the path to appear partially drawn or with gaps, and forgetting to set fill to none for a line-drawing effect, causing the path's interior to fill immediately and spoil the drawing illusion. Another mistake is animating dashoffset with a transition on a path whose dasharray is set in CSS but whose length changes, such as a responsive path that scales, because scaling changes the effective length but not the dasharray value, causing the animation to misalign.

Finally, stroke animation should follow the same principles as other motion: keep durations reasonable, respect reduced motion, and ensure the final state is correct even if the animation is interrupted. A line-drawing animation that is interrupted should leave the path fully drawn or fully hidden, not partially drawn in a way that looks like a broken chart, so its end state must be set explicitly on cancellation or on reduced motion. Stroke animation is a powerful way to make SVG feel alive, because it turns a static shape into a process, but that power requires precise length handling and careful end-state management to be reliable.

Check: How does animating stroke-dashoffset make a path appear to draw itself?

Section 45 — Responsive and adaptive motion

Responsive motion means motion that adapts to screen size, device capability, and user preference, rather than playing identically everywhere. A slide that works on a desktop, where there is room for a panel to slide in from the side, may not work on a phone, where the same panel should slide up from the bottom as a sheet, because side panels on phones cover too much width or feel cramped. A stagger that works for a grid on desktop, where items are visible together, may need to be shortened or removed on a phone, where items are stacked and the stagger delays content the user is scrolling to. Adapting motion to context is therefore part of responsive design, not a separate concern, and it should be planned alongside layout breakpoints.

Device capability also affects motion. A phone with a slower processor or a low battery may struggle with complex animations that run smoothly on a desktop, so motion should degrade gracefully on less capable devices, by simplifying animations, reducing particle counts, or disabling non-essential motion.

User preference is the most important adaptation, and it includes reduced motion, as covered earlier, but also other preferences, such as a preference for reduced data or for high contrast, which may affect motion indirectly. A user who prefers reduced motion should get instant state changes, not merely slower animations, because slower motion is still motion, and it can still cause discomfort. A user who prefers high contrast may need motion indicators that do not rely on subtle colour changes, such as a focus ring that animates in size as well as colour. Those preferences are not edge cases; they are part of the user base, and motion that ignores them excludes readers who have explicitly expressed a need.

Implementation for responsive motion can use media queries, including width queries for layout-based changes, and prefers-reduced-motion for preference-based changes, with CSS custom properties for motion tokens that change per breakpoint or per preference. For example, a motion token for slide direction could change from horizontal to vertical at a small breakpoint, or a duration token could become near zero under reduced motion. JavaScript motion should also check those conditions, by reading media queries with matchMedia, and adapting its behaviour accordingly, rather than assuming a single motion design works everywhere. That shared token approach keeps CSS and JavaScript motion consistent across contexts.

Finally, responsive motion should be tested across contexts, not just at one size or on one device. Test each significant animation at desktop, tablet, and phone sizes, on a real phone, with reduced motion enabled, and with a keyboard, confirming that the animation adapts appropriately and that content remains available and usable in every context. An animation that works beautifully on desktop but breaks on a phone, or that respects reduced motion but not touch, is not a responsive animation; it is a desktop animation that happens to run elsewhere. Responsive motion, like responsive layout, is about serving the user in their actual context, not about scaling a single design to fit all contexts.

Check: Why should reduced motion mean instant state changes rather than slower animations?

Section 46 — Motion in dark mode and themes

Motion in dark mode and in different themes is largely the same as in light mode, but its perception changes, because contrast, brightness, and colour interact with motion in ways that can make an animation feel stronger or weaker. A bright element moving across a dark background, for example, can appear more prominent and more distracting than the same element moving across a light background, because the contrast is higher and the eye is drawn to bright movement in darkness. Conversely, a subtle fade that is visible in light mode may be nearly invisible in dark mode, if its colours are too close to the dark background. Those perceptual differences mean motion should be tested in every theme, not just in the default theme, and adjusted where necessary.

Colour transitions in dark mode deserve attention. A button that transitions from a light background to a slightly darker light background in light mode may need a different transition in dark mode, where the corresponding colours are dark greys that differ less in lightness.

Shadows and glows, which are often animated for depth or emphasis, also behave differently in dark mode. A shadow that adds depth in light mode may be invisible in dark mode, because shadows are dark on dark, while a glow that is subtle in light mode may be glaring in dark mode, because bright glows stand out against dark backgrounds. If an animation relies on a shadow or glow to convey its meaning, such as a card lifting on hover, that meaning may be lost or exaggerated in the other theme. A safer approach is to convey lift with a combination of shadow, border, and transform, so the transform and border carry the meaning even when the shadow is less visible, and to test the effect in both themes, adjusting shadow opacity and glow intensity per theme as needed.

Reduced motion and theme preferences interact as well. A user may prefer both dark mode and reduced motion, and the interface should honour both, with instant state changes in dark mode just as in light mode. That combination should be tested, because a reduced-motion stylesheet that overrides animations in light mode may need to be checked in dark mode as well, to ensure it does not inadvertently restore motion or break dark mode colours. The principle is that preferences compose: each preference should be honoured independently, and their combination should be a usable interface, not a broken one, which requires testing combinations, not just individual preferences.

Finally, motion in themes should be documented in the design system, with theme-specific colour values for animated states and with notes on which effects need adjustment per theme. That documentation prevents a common bug where a new component's motion is designed in light mode and never tested in dark mode, until a user reports that its hover feedback is invisible or its celebration is blinding. Themes are not just colour swaps; they change how motion is perceived, and motion design must account for that, by testing, by using multiple signals for state changes, and by keeping motion tokens consistent while allowing colour values to adapt per theme.

Check: Why can a bright element moving on a dark background feel more distracting than on a light background?

Section 47 — State machines for animation

A state machine is a model for an interface element's states and the transitions between them, and it is a valuable tool for animation, because many animation bugs come from unmodelled states or transitions. Consider a modal: it can be closed, opening, open, closing, and, in some designs, minimised or dragging. Each state has a visual appearance and a set of allowed transitions: closed can go to opening, opening can go to open or to closing if interrupted, open can go to closing, and closing can go to closed. If you do not model those states explicitly, you may end up with code that assumes a modal is either open or closed, and that breaks when it is opening or closing, because an animation is in progress and the element is in an intermediate state that the code does not handle.

Modelling states explicitly, even informally, helps you handle interruption, which is the most common animation bug. When a user triggers a transition while another is in progress, a state machine can define what should happen: should the new transition cancel the old one and start from the current state, should it queue after the old one, or should it be ignored until the old one completes? Each choice is valid for different cases, but the choice should be deliberate, not accidental.

State machines also help with animation code organisation. Instead of scattering animation logic across event handlers, each of which toggles classes or starts animations independently, you can centralise it in a function that takes the current state and a desired state, and returns or performs the transition, including its animation, its duration, and its cleanup. That central function can handle common concerns, such as cancelling a previous animation, setting final states on completion, and updating accessibility attributes, such as expanded or hidden, in one place. That centralisation makes animation code easier to test, because you can test transitions between states in isolation, without simulating user events, and easier to maintain, because a change to a transition affects one place, not many handlers.

For complex interfaces, formal state machine libraries exist, which let you define states, transitions, guards, and actions declaratively, and which can generate diagrams and tests from the definition. Those libraries are valuable for large, stateful interfaces, such as multi-step forms, media players, or games, where the number of states and transitions grows quickly and informal modelling becomes error-prone. For simpler interfaces, an informal state machine, such as a variable holding the current state and a switch statement handling transitions, is sufficient, provided it is explicit and documented.

Finally, state machines help with accessibility and with reduced motion, because state changes, not animations, are the source of truth. When reduced motion is active, a state machine can transition instantly to the target state, without animation, while still updating all the same state attributes and focus management, because the state transition logic is separate from the animation logic. That separation ensures that the reduced-motion version is not a broken version, but the same state machine with a different animation policy, which is the correct way to handle reduced motion. State machines turn animation from a collection of effects into a modelled behaviour, which is more reliable, more testable, and more accessible.

Check: Why does an explicit state model help handle interrupted animations?

Section 48 — Interruptible animations

Interruptible animations are animations that can be stopped, reversed, or retargeted mid-way, without jumping or leaving the element in a broken state, and they are essential for responsive interfaces, because users frequently change their minds mid-animation. A user may hover over a button, then move away before its hover animation completes; they may open a modal, then close it before its entrance completes; they may start a drag, then release it early. If those animations cannot be interrupted cleanly, they will either complete despite the user's change of mind, causing a delay, or jump abruptly to the new state, causing a visual glitch. Either outcome feels unresponsive, even if the animation itself was smooth in isolation.

Making a CSS animation interruptible requires care with fill modes and class management. When a class that triggers an animation is removed, the animation stops, and the element returns to its base styles, which may be a jump if the animation was partway through. To make the interruption smooth, the animation should be a transition rather than a keyframe where possible, because transitions naturally animate from the current value to the new value, even if the current value is intermediate.

For JavaScript animations, interruption is handled by cancelling the current animation, using cancelAnimationFrame or the Animation object's cancel method, and starting a new animation from the current values, which are stored in variables or read from the element. That requires keeping current values accessible, not just target values, because the new animation's start is the old animation's current position, not its original start or its intended end. If you start the new animation from the original start, the element will jump back, which is the classic interruption bug. If you start it from the intended end of the old animation, it will jump forwards. Only starting from the current value gives a smooth interruption, which is why current values must be tracked.

A related concept is velocity preservation, where an interrupted animation's velocity is carried into the new animation, so the new motion continues smoothly rather than starting from rest. For example, a panel that was sliding in quickly and is then dismissed should slide out with a velocity that reflects its incoming speed, not start its exit slowly from rest, because starting from rest after fast incoming motion feels like a hitch. Velocity preservation is more complex to implement, and it is most valuable for gesture-driven motion and for springs, where velocity is a natural part of the model. For simpler transitions, starting from the current value with a standard easing is usually sufficient, and velocity preservation can be added later if testing shows a hitch.

Finally, test interruptibility explicitly, by triggering animations and interrupting them at various points: early, midway, and near completion, and by triggering the reverse animation immediately, and by triggering the same animation repeatedly in quick succession. Those tests reveal jumps, stacking, and state errors that single, uninterrupted tests miss. An animation that is smooth when uninterrupted but jumpy when interrupted is not a finished animation, because real users interrupt animations constantly, and the interface's responsiveness is judged more by its interrupted behaviour than by its ideal behaviour. Interruptibility is therefore a requirement, not a refinement, for professional motion design.

Check: Why must a retargeted animation start from the current value, not from its original start?

Section 49 — When not to animate

Knowing when not to animate is as important as knowing how to animate, because unnecessary motion taxes attention, delays tasks, and can cause discomfort, without adding value. A good rule is to animate only when motion helps the user understand a change, confirm an action, or find content, and to leave static what is already clear without motion. Content that is already visible and understandable, such as a paragraph of text, a static label, or a simple list, does not need an entrance animation, because the user can read it immediately without motion, and animating it merely delays reading. Similarly, a change that is already obvious, such as a large heading changing, does not need motion to draw attention to it, because its size and position already do so.

Routine, frequent actions are a second case where animation should be minimal or absent. Switching tabs, moving between list items, or typing in a field are actions users perform many times, and even a short animation for each, when repeated, becomes tiresome and slows the user down cumulatively. For those actions, instant state changes, or very short transitions of around one tenth of a second, are better than elaborate animations, because the user's goal is efficiency, not delight.

Content that must be read carefully is a third case. Instructions, error messages, legal text, and data tables should appear promptly and remain stable, because motion during reading disrupts comprehension and can cause the user to lose their place. An error message that slides in, bounces, or fades slowly may draw attention, but it also delays the moment the user can read and act on it, and a table whose rows animate in one by one delays scanning and comparison. For such content, a brief, subtle entrance, or no entrance at all, is better, because readability is the priority, and motion that interferes with reading has failed, however attractive it looks in isolation.

Accessibility and context are a fourth case. When reduced motion is active, most non-essential motion should be disabled, as covered throughout this chapter. When a user is on a slow device, in a low-bandwidth context, or using a screen reader, motion adds cost without benefit, because it cannot be perceived as intended, or it competes with other resources. When motion would flash, move large areas, or loop continuously, it should be avoided or made pausable, because of seizure and vestibular risks. Those are not optional considerations; they are constraints that override aesthetic preferences, and a design that ignores them is incomplete, however polished it appears to a typical user.

Finally, when in doubt, leave it static. A static interface that is clear, fast, and accessible is better than an animated interface that is confusing, slow, or uncomfortable, because clarity, speed, and accessibility are the core qualities of a good interface, while motion is an enhancement to those qualities, not a substitute for them. Motion should be added deliberately, with a purpose, a modest duration, and a clear end state, and it should be removed if testing shows it does not help. That restraint is the mark of mature motion design: not the ability to animate everything, but the judgement to animate only what benefits from motion, and to leave the rest still.

Check: Why should routine, frequent actions have minimal or no animation?

Section 50 — Animations and motion design in one paragraph

Motion design is the use of movement to communicate change, confirm actions, guide attention, and make interfaces feel responsive, and this chapter has covered its tools, principles, and limits in full. CSS transitions animate two-state changes, such as hover and focus, with durations, easings, and delays that should be short and consistent, while keyframe animations handle multi-step sequences, loops, and entrances, with fill modes and play states that control their start and end. Transforms and opacity are the performant properties to animate, because they avoid layout and paint, while layout properties, shadows, and gradients should be avoided where a transform or opacity equivalent exists, and sixty frames per second, with sixteen milliseconds per frame, is the budget that judges smoothness. Easing shapes how motion feels, stagger and sequencing order multiple motions into a coherent choreography, and tokens for duration, easing, and delay keep motion consistent across a product, just as colour and spacing tokens do for static design.

Beyond CSS, JavaScript motion through the Web Animations API and requestAnimationFrame gives programmatic control for dynamic, data-driven, and gesture-driven motion, with elapsed-time calculations, interpolation, and easing functions that must be handled carefully to remain smooth and interruptible. Techniques such as FLIP animate layout changes with transforms, scroll-triggered animation uses Intersection Observer rather than scroll listeners, and SVG and canvas animation each have their own patterns, from stroke-dashoffset drawing to full redraw loops, all sharing the same performance and accessibility principles. Modal, accordion, carousel, and navigation motion each have specific patterns, from focus management to height animation workarounds to directional slides, and each must be tested with keyboard, touch, and screen readers, not just with a mouse, because motion that works for only one input method is incomplete.

Above all, motion must be restrained, purposeful, and accessible. It should respect prefers-reduced-motion by showing final states instantly, avoid flashing and large, continuous movement, provide pause controls for auto-playing content, and never gate essential content behind an animation, delay feedback, or celebrate routine actions with large effects. Durations should stay within a modest range, frequent actions should be fastest, and when motion does not help the user understand or act, it should be left out, because a clear, fast, static interface is better than a confusing, slow, animated one. Carry forward a simple test for every animation you propose: what does it help the user understand or do, can it run smoothly on a phone, can it be interrupted cleanly, and does it remain clear with reduced motion and with a keyboard? If it passes those tests, it earns its place; if it does not, simplify or remove it, and let stillness do the work. When you review a finished interface, watch it once without interacting, then use it quickly as a real user would, because the first view reveals whether motion guides attention, and the second reveals whether it delays, distracts, or breaks under speed and interruption.

Check: In your own words, what test should every animation pass before it earns its place?

Practical task

Build a small interface with a modal, an accordion, and a list, and animate each with CSS: a modal that fades and scales in with a backdrop, an accordion that expands smoothly without clipping, and a list whose items stagger in on load. Use motion tokens for durations and easings, respect prefers-reduced-motion by showing all content instantly when it is active, and test it: trigger each animation, interrupt it midway, navigate it by keyboard, and check frame smoothness on a phone-sized window, fixing any jump, clip, or focus problem you find.

Chapter questions

  1. A button hover transition is defined only on the hover rule. What goes wrong when the pointer leaves, and how do you fix it?
  2. An accordion expands slowly for short content and clips long content. What is causing both problems, and what better approach would you use?
  3. A carousel auto-plays and cannot be paused. What accessibility and usability problems does that create, and how would you fix them?
  4. Explain FLIP in your own words, including why it animates with transforms rather than layout properties.
  5. A page transition takes one second and plays for every tab switch. Why is that a problem, and what duration and approach would you choose instead?