Animating Stack Changes Without Breaking the DOM
CSS animation ranges from trivial (a button hover) to genuinely hard: animating an element’s size and position while other elements shift to accommodate it. The classic case is a stack of items where one is removed — the remaining items need to settle into the vacated space with motion that feels physical, not abrupt.
The problem is that CSS alone cannot fully animate elements as they are added to or removed from the DOM. A removed <li> disappears instantly; a new one appears with no intermediate state to transition from. You can mask this with a quick fade, but the list still snaps shut underneath, which can disorient users who expect natural, real-world movement.
This article walks through three animation patterns for handling dynamic list changes — fade-and-slide, collapse, and side-slide — each built on the same DOM structure and JavaScript hooks. We also cover the performance and accessibility considerations that keep these patterns production-ready.
The Shared DOM Structure
All three techniques rely on a modified structure: each .list-item is wrapped in a parent .list-container. The container is the element that animates its height; the list item inside handles its own visual state.
<ul class="list">
<li class="list-container">
<div class="list-item">Item 1</div>
</li>
...
<button class="add-btn">Add item</button>
</ul>
Spacing between items uses margin-top on all but the first container via an enabling selector — .list-container:not(:first-child). This prevents the top item from being pushed down and avoids double margins when a container collapses to zero height.
Each .list-item is positioned absolute relative to its container. This decouples the item’s visual movement from the container’s height animation. When a container collapses, the item can float away independently while sibling containers shift up to fill the gap.
Measuring Heights with JavaScript
Because absolutely positioned children don’t give their parents height, the container must be sized explicitly. All list items share identical dimensions, so we measure just the first one and write the result as a new CSS rule via JavaScript:
const listItems = document.querySelectorAll('.list-item');
const setContainerHeights = () => {
const firstItem = listItems[0];
const itemHeight = firstItem.clientHeight;
const styleTag = document.createElement('style');
styleTag.innerHTML = `.list-container { height: ${itemHeight}px; }`;
document.body.prepend(styleTag);
};
Inline <style> rules override external stylesheet declarations, so the height applies without needing a utility class on each element.
Technique 1: Slide-Down Opacity
In this pattern, new items fade and float into place; removed items fade out while their container collapses. The trick is managing two states per element — one visible (with a .show class) and one hidden — and transitioning between them.
Without .show, a container has zero height and no visible content. Adding .show to both container and item triggers a transition on height (container) and transform/opacity (item). The transform moves the item vertically from an offset back to its resting position as it fades in.
A critical timing detail: when adding a new item, the .show class cannot be applied synchronously with insertion, or the browser has no starting state to transition from. The item would simply appear. Delaying by 15 ms via setTimeout gives the browser a frame to register the initial hidden state, then the class change animates smoothly.
Removal follows the inverse path. The click handler must identify the actual .list-container (the event target is likely the child .list-item), so we walk up the DOM until we find it:
while (!container.classList.contains('list-container')) {
container = container.parentNode;
}
Then .show is removed from both container and item, starting their transitions.
Cleanup and Compatibility
A faded-out element is still in the DOM unless explicitly removed. Leaving it there accumulates dead weight, so the container uses ontransitionend to detach itself only after the collapse animation finishes — avoiding any interruption of the visual effect.
Semantic tags help, but assistive devices may not track dynamic list changes. Adding aria-live="assertive" to the parent .list forces screen readers to announce changes immediately, interrupting any current readout. This is a pragmatic compromise; native list semantics are always preferable when available.
Technique 2: Collapse Animation
This variant is more understated: items simply collapse or expand in place while fading, with no vertical movement. The rest of the list repositions as the container’s height animates.
The implementation change is minimal. Set overflow: hidden on .list-container so the child doesn’t spill out as the container shrinks, and remove the transform from the .show state on the item. Everything else — the state toggling, the height measurement, the event wiring — stays the same.
Technique 3: Side-Slide Animation
The third pattern desyncs the container and item animations. On removal, the item slides out to the right while its container collapses simultaneously; the item is visible during the exit, sliding across the top of the list as items below move up. On addition, the container first expands to make vertical room, then — after a delay — the item slides in from the right.
CSS here mirrors the slide-down technique but transitions on the x-axis instead of the y-axis. The JavaScript adds layered setTimeout calls to sequence the motion properly.
For addition, the container’s .show class is applied immediately so its height animates open. The item’s .show is applied 350 ms later, giving the container enough time to open and clear vertical space before the new item slides in.
Removal reverses the sequence. The item’s .show is removed first so it begins sliding out right away. The container’s .show is removed 350 ms later — while the item is still mid-exit — so the list below can start moving up. Finally, after another 600 ms (950 ms total), the .show class removal triggers ontransitionend and the container is removed from the DOM, as both animation phases complete.
Choosing an Approach
These three patterns share the same core mechanics — wrapped elements, measured heights, the .show toggle and transition timing — but produce distinctly different feels: a floating fade, an in-place collapse, or a sequenced slide. Each is a template you can adapt to custom easing, durations, or movement axes without restructuring the underlying list.



