Command Palette

Search for a command to run...

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:

  1. Trigger: A class change or style update occurs.
  2. Transition: The browser interpolates values over a period (e.g., 300ms).
  3. 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

MethodBest forComplexityPerformance
Manual JSSimple projects with no librariesLow (but brittle)Good
Framer MotionComplex UI, nested animationsMediumModerate
Modern CSSNative performance, minimal JSMediumExcellent

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.