The hidden mechanics of smooth component unmounting
mar 13, 2026
|5 min read
Ever happened, when you click a button to close a modal, for a split second, the content snaps to its final state before fading out? or worse, the modal disappears entirely, and then the overlay fades.
this jarring behavior is a classic symptom of a mismatch between how JavaScript manages state and how CSS animations handle their lifecycle.
in this blog, we'll dive deep into the technical reasons behind this flicker and explore the robust solutions that ensure your components exit as smoothly as they enter.
The root cause: Lifecycle conflict
To build a robust solution, we must first understand the synchronization failure between two distinct systems.
1. Synchronous state management
In frameworks like React, unmounting is typically handled by a boolean state:
const [isOpen, setIsOpen] = useState(true);
const handleClose = () => {
setIsOpen(false); // Trigger immediate unmount
};when isOpen becomes false, the component is immediately removed from the Virtual DOM.
React then instructs the browser to remove the corresponding DOM node.
this happens synchronously and instantly.
2. The CSS animation timeline
Unlike JavaScript, CSS animations and transitions are asynchronous.
they operate on their own timeline:
- Trigger: A class change or style update occurs.
- Transition: The browser interpolates values over a period (e.g.,
300ms). - Completion: The element reaches its final state.
the "flicker" occurs because React removes the DOM node before the CSS timeline can complete.
even if you use animation-fill-mode: forwards, the element is deleted from the tree while the browser is still processing the visual exit.
Solution 1: Delayed unmounting (manual)
The most basic fix is to decouple the "logical" close from the "physical" unmount.
this involves introducing an intermediate state (e.g., isExiting) and using a setTimeout that matches your CSS duration.
const [status, setStatus] = useState('open'); // 'open', 'exiting', 'closed'
const handleClose = () => {
setStatus('exiting');
setTimeout(() => setStatus('closed'), 300); // Wait for animation
};while functional, this approach is prone to "magic number" errors where the JS timeout and CSS duration fall out of sync.
Solution 2: Framer Motion (orchestration)
For React applications, AnimatePresence is the industry standard for handling this complexity.
it effectively "pauses" the unmounting process, allowing the component to remain in the DOM until its exit animation completes.
<AnimatePresence>
{isOpen && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
>
Content
</motion.div>
)}
</AnimatePresence>Pros: Handles nested animations, robust, and highly customizable.
Cons: Adds to your JS bundle size.
Solution 3: Modern CSS (discrete transitions)
Historically, you couldn't transition the display property.
however, modern CSS and explicitly Tailwind 4 have introduced transition-behavior: allow-discrete and the @starting-style rule.
this is the exact strategy I used for the Post Share Menu on this site.
when working with libraries like Radix UI or shadcn/ui, components often use a data-state attribute to handle transitions.
by combining allow-discrete with animation-fill-mode, we can ensure the element stays in the DOM long enough for the exit animation to finish:
/* Real-world example from my portfolio */
.menu-content[data-state="closed"] {
animation-fill-mode: forwards;
transition-behavior: allow-discrete;
}
@starting-style {
.menu-content[data-state="open"] {
opacity: 0;
transform: translateY(10px);
}
}by using allow-discrete, the browser keeps the element in the layout for the duration of the transition, even if the target state is hidden or none.
this is a massive win for performance as it requires zero JavaScript for unmounting orchestration.
Cheat sheet: Choosing the right method
| Method | Best for | Complexity | Performance |
|---|---|---|---|
| Manual JS | Simple projects with no libraries | Low (but brittle) | Good |
| Framer Motion | Complex UI, nested animations | Medium | Moderate |
| Modern CSS | Native performance, minimal JS | Medium | Excellent |
You might notice that the unmounting flicker is highly visible on a desktop browser but seems "perfectly smooth" when viewed on a mobile device.
this is a common trap! mobile browser engines often use more aggressive compositing and frame-skipping optimizations to save battery.
while this might hide the glitch on your phone, the underlying logical error (the race condition) still exists.
a premium experience means ensuring consistency.
a user on a high-refresh-rate desktop monitor or an older laptop should see the same polished transition as a mobile user.
Why smooth unmounting matters for SEO & UX
You might wonder, does a flicker really matter for SEO? Indirectly, yes.
search engines like Google use Core Web Vitals and User Experience signals as ranking factors.
a glitchy UI increases bounce rates and reduces dwell time.
premium micro-interactions, like the ones I implemented for draftlogo, tell your users (and Google) that your site is high-quality and trustworthy.
Final thoughts
Achieving smooth unmounting requires a shift from thinking about "hiding" elements to "orchestrating" their exit.
- For performance: Leverage modern CSS (
allow-discrete) to minimize JavaScript overhead. - For complex interactions: Use lifecycle-aware libraries like Framer Motion to handle nested exit animations.
- For consistency: Ensure your animation durations are defined as central tokens to avoid synchronization issues between CSS and JavaScript.
refining these exit mechanics transforms a functional interface into a polished, premium product that feels responsive and intent-driven.