Recommended Free Tools
React dialog exit animations are usually cut off because the dialog is unmounted as soon as open becomes false. Once the content and overlay DOM nodes are gone, CSS has nothing to animate and a JavaScript animation library cannot observe a leaving child. Keep the dialog’s lifecycle boundary mounted, then let a presence or transition manager delay final removal until the close animation finishes.
What actually cuts off the closing animation?
React does not animate an unmount by itself. A render such as {open && <Dialog />} removes the entire dialog from the React tree during the render that changes open to false. The browser then removes the corresponding DOM nodes before a closing transition or keyframe can be seen.
As an Amazon Associate I earn from qualifying purchases.
The same problem appears one level higher. If an ancestor containing the dialog is conditional, the ancestor can disappear even when the dialog component has its own close state. An animation library has the same requirement in a different form: its exit manager must remain mounted long enough to detect that a child has left. Motion documents that AnimatePresence observes direct children removed from the React tree; if the AnimatePresence boundary is removed at the same time, it has no opportunity to run the child’s exit animation (Motion AnimatePresence documentation).
Do these 3 things before closing this tab:
1Clear out junk files and repair common Windows errors2Fix the driver behind crashes, sound loss and screen glitches3Repair Windows errors before they cause bigger problemsDialog primitives add another lifecycle layer. Radix Dialog ties its portal, overlay and content presence to the open state, and exposes forceMount when an application needs an animation library to control when those parts are removed (Radix Dialog source; Radix Dialog documentation). The visual animation and the dialog’s focus, overlay and accessibility behavior therefore have to be coordinated rather than solved by hiding a still-interactive element.
#1 Best Overall
Diagnose the problem before changing code
- Find the first conditional. Search for
open &&, a ternary that returnsnull, or an ancestor that is rendered only while the dialog is open. That is the node being removed immediately. - Identify the exit owner. Decide whether Radix Presence, a CSS animation, Motion’s
AnimatePresence, React Transition Group, or another manager is responsible for delaying removal. Two owners fighting over the same node often produce an abrupt close. - Inspect the DOM during close. In browser developer tools, trigger close and watch whether the content and overlay remain in the DOM. If they vanish in the same frame as the state change, no animation rule can fix the timing.
- Check the rendered state. Radix exposes
data-state='open'anddata-state='closed'on relevant parts. Confirm that the closed selector is present and that its animation name is not overridden. - Check direct-child placement. For Motion, the element that receives
exitmust be a direct child ofAnimatePresence. A fragment or wrapper that is removed instead can hide the change from the boundary. - Check keys and refs. Dynamically replaced children need stable, distinct keys. In Motion
popLayoutmode, a custom immediate child must forward a ref to the DOM element that is being taken out of layout (Motion AnimatePresence documentation). - Check focus while waiting. A fix that leaves a closed dialog tabbable or traps focus behind an invisible overlay is not a correct fix. Radix documents modal focus trapping, announcements and focus restoration as part of its dialog behavior (Radix Dialog documentation).
Choose one component to control final removal
The implementation becomes predictable when one lifecycle manager owns the decision about when a leaving node is finally unmounted.
| Approach | Animation mechanism | Who delays removal? | Integration details |
|---|---|---|---|
| Radix Dialog with CSS keyframes | CSS @keyframes selected from Radix state attributes |
Radix Presence observes the open/closed lifecycle | Keep the Radix parts in place; verify the closed animation for the installed version |
| Radix Dialog with a JavaScript library | JavaScript-controlled opacity, transforms or variants | The animation boundary, with Radix parts mounted as needed | Use forceMount on supported parts and let the animation finish before unmounting |
Motion AnimatePresence |
Motion’s initial, animate and exit values |
AnimatePresence retains direct children during exit |
The boundary must stay mounted; use stable keys and forward refs in popLayout mode |
| React Transition Group | CSS or JavaScript callbacks driven by transition states | A transition component manages entering, leaving and unmount timing | Use the API for the installed release; the project notes that v2 and later are not backward-compatible with v1 |
Do not treat a CSS transition declaration as a lifecycle manager. A transition interpolates between two styles only while the element exists. A keyframe animation can describe a complete close sequence, but the element still has to remain mounted until that sequence is complete.
Fix a Radix Dialog using CSS keyframes
Radix is the simplest route when the dialog can use CSS for its open and close visuals. Keep the root rendered and control its open state with the documented props:
import * as Dialog from '@radix-ui/react-dialog';
export function SettingsDialog({ open, setOpen }) {
return (
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Trigger>Settings</Dialog.Trigger>
<Dialog.Portal>
<Dialog.Overlay className='DialogOverlay' />
<Dialog.Content className='DialogContent'>
<Dialog.Title>Settings</Dialog.Title>
<Dialog.Close>Close</Dialog.Close>
</Dialog.Content>
</Dialog.Portal>
</Dialog.Root>
);
}
Use the state attributes on the overlay and content to select separate keyframes. The exact presence behavior can vary with the Radix version you have installed, so verify the version-specific animation guidance alongside the component API.
.DialogOverlay[data-state='open'] {
animation: overlayIn 180ms ease-out;
}
.DialogOverlay[data-state='closed'] {
animation: overlayOut 140ms ease-in forwards;
}
.DialogContent[data-state='open'] {
animation: dialogIn 220ms cubic-bezier(.16, 1, .3, 1);
}
.DialogContent[data-state='closed'] {
animation: dialogOut 160ms ease-in forwards;
}
@keyframes overlayIn {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes overlayOut {
from { opacity: 1; }
to { opacity: 0; }
}
@keyframes dialogIn {
from { opacity: 0; transform: translateY(8px) scale(.98); }
to { opacity: 1; transform: translateY(0) scale(1); }
}
@keyframes dialogOut {
from { opacity: 1; transform: translateY(0) scale(1); }
to { opacity: 0; transform: translateY(8px) scale(.98); }
}
Do not add a second {open && ...} around Dialog.Portal, Dialog.Overlay or Dialog.Content if Radix is supposed to observe their closed state. That conditional removes the node before Radix Presence can retain it. Also avoid setting display: none or pointer-events rules that make the closing element interactive while it is leaving; the dialog primitive should continue to manage focus and modality.
Give a JavaScript animation library control with Radix
Use this path when the animation needs JavaScript-driven variants, sequencing or callbacks. Radix documents forceMount for cases where an animation library needs more control over mounting and unmounting (Radix Dialog documentation).
Mount the Radix parts with forceMount, place the visual element under the animation boundary, and make the open boolean the animation boundary’s condition. The exact wrapper arrangement depends on the library, but the ownership rule is constant: Radix must not remove the node before the animation manager has finished.
<Dialog.Root open={open} onOpenChange={setOpen}>
<Dialog.Portal forceMount>
<AnimatePresence>
{open ? (
<Dialog.Overlay forceMount asChild>
<motion.div
key='overlay'
className='DialogOverlay'
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
exit={{ opacity: 0 }}
/>
</Dialog.Overlay>
) : null}
</AnimatePresence>
<AnimatePresence>
{open ? (
<Dialog.Content forceMount asChild>
<motion.div
key='content'
className='DialogContent'
initial={{ opacity: 0, y: 8, scale: .98 }}
animate={{ opacity: 1, y: 0, scale: 1 }}
exit={{ opacity: 0, y: 8, scale: .98 }}
>
...
</motion.div>
</Dialog.Content>
) : null}
</AnimatePresence>
</Dialog.Portal>
</Dialog.Root>
Before using this exact composition, check the installed Radix and animation-library APIs: asChild changes which element receives props and refs, and the overlay and content may need to be grouped under one boundary if they must leave together. The important checks are that every required Radix part supports forceMount, the animated DOM node receives the dialog props and ref, and the boundary remains mounted while its direct child exits.
Fix Motion exit animations with a permanent AnimatePresence boundary
Motion detects a direct child leaving the React tree and keeps it around for its exit animation. The common mistake is to conditionally render the boundary itself:
{open && (
<AnimatePresence>
<motion.div exit={{ opacity: 0 }} />
</AnimatePresence>
)}
When open changes, both the boundary and its child disappear. Keep AnimatePresence unconditional and put the condition inside it:
<AnimatePresence>
{open && (
<motion.div
key='dialog'
role='dialog'
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: 12 }}
>
Dialog content
</motion.div>
)}
</AnimatePresence>
If a dialog can be replaced by another dialog or panel, give each direct child a stable key that changes when the identity changes. Without distinct keys, Motion may treat the new content as an update rather than a leaving child. Keep the key stable for the lifetime of one dialog instance; do not generate a new random key on every render.
What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
When using popLayout, a custom component immediately below AnimatePresence must forward its ref to the actual DOM element Motion should remove from layout. Otherwise Motion cannot apply the layout-pop operation correctly (Motion AnimatePresence documentation). This ref requirement is separate from the basic exit problem, but it commonly appears when a dialog is wrapped in a design-system component.
Rank #3
Use React Transition Group when it owns the transition state
React Transition Group provides components that manage mounting and unmounting states over time for animation (React Transition Group repository). A typical pattern keeps the transition component mounted and lets its in prop describe whether the dialog is present:
import { CSSTransition } from 'react-transition-group';
<CSSTransition
in={open}
timeout={180}
classNames='dialog'
unmountOnExit
>
<div role='dialog' aria-modal='true'>
Dialog content
</div>
</CSSTransition>
unmountOnExit should be owned by the transition component, not by a surrounding open && expression. The timeout and CSS animation duration must describe the same interval; if the transition manager is told to finish sooner than the CSS animation, removal still looks clipped. If JavaScript callbacks determine completion, call the library’s completion hook only after the visual animation has ended.
Confirm the API against the version installed in your project. The project states that its v2-and-later API is not backward-compatible with v1 and that the v1 codebase is no longer updated (React Transition Group repository).
Preserve dialog accessibility during the visual exit
A dialog is more than a box that fades out. Radix’s documented behavior includes modal focus trapping, screen-reader announcements, an overlay and focus restoration (Radix Dialog documentation). A visual workaround must not leave a closed dialog available to keyboard or assistive-technology users.
- Keep focus ownership explicit. When close starts, focus should follow the dialog primitive’s restoration behavior. Do not move focus to an element hidden by the closing animation.
- Do not leave an invisible modal backdrop active. A transparent overlay that still intercepts clicks or traps focus after the user considers the dialog closed creates a second, accessibility-related bug.
- Animate the semantic parts together. If content fades out but the overlay remains modal, users may be unable to interact with the page during the remainder of the animation. If the overlay vanishes first, the dialog can appear detached from its modality. Coordinate both timelines deliberately.
- Keep labels and roles on the actual animated element. Wrapping a Radix part with a JavaScript animation component can drop refs or props unless the wrapper forwards them.
- Respect reduced motion. Provide a reduced-motion rule that shortens or removes visual movement while retaining the same mount and focus lifecycle. Do not replace lifecycle management with a permanently hidden but interactive node.
Common failed fixes and the precise repair
Conditioning the whole dialog on open
Symptom: The dialog disappears on the same frame as the close click.
Repair: Keep the root and the chosen animation boundary mounted. Put the open condition inside the boundary, or let the dialog primitive’s Presence implementation observe the state.
Rank #4
Putting AnimatePresence inside the conditional
Symptom: The exit prop is present, but no exit animation runs.
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchPC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Repair: Render AnimatePresence unconditionally and make the animated dialog its direct conditional child.
Using a CSS transition without a retained node
Symptom: Changing opacity or transform appears to work when opening but not closing.
Repair: Delay unmount through Radix Presence, Motion, React Transition Group or another transition manager. A CSS transition cannot animate a node that has already been removed.
Applying forceMount and never handing removal back
Symptom: The animation plays, but the closed dialog remains in the DOM and may interfere with focus or pointer input.
Repair: Treat forceMount as permission for your animation layer to own removal, not as a permanent hidden state. Ensure the animation boundary actually unmounts the leaving part after completion.
Best Value
Animating a wrapper while the primitive removes its child
Symptom: The wrapper remains briefly, but the dialog content vanishes inside it.
Repair: Put the animation on the DOM element whose lifecycle is being retained, or use the primitive’s composition API so the animated element receives the primitive’s props and ref.
Mismatched durations
Symptom: The last few frames are clipped, or the dialog stays mounted noticeably longer than the animation.
Quick wins for a faster PC:
Clear out junk files and repair common Windows errorsFree Scan →Scan for outdated or missing drivers - takes under a minuteDriver Scan →Repair Windows errors before they cause bigger problemsFix Now →Repair: Use one source of truth for duration. Match Motion’s transition duration, a transition component’s timeout and the CSS keyframe duration, including any delay. If separate overlay and content animations have different lengths, the owner must wait for the longer one.
Changing the key on every render
Symptom: The dialog repeatedly re-enters or never completes a clean exit.
Repair: Use a stable key for one dialog instance and change it only when the displayed item genuinely changes.
A reliable verification procedure
- Open the dialog and confirm the expected focus target, title announcement and overlay.
- Start closing it with the close button, Escape and any outside-click behavior your design permits. All paths should use the same open-state transition.
- Watch the content and overlay in developer tools. They should remain mounted for the intended exit interval and then disappear.
- Use a slow-motion setting or temporarily lengthen the animation to verify that the closed state is actually rendered.
- Tab repeatedly while closing. Focus must not escape into an invisible modal or become trapped after the dialog is gone.
- After removal, inspect the document for stale overlays, duplicate dialog IDs and nodes that still receive pointer events.
- Test a reduced-motion preference and confirm that shortening the visual effect does not bypass focus restoration or final unmounting.
- Test rapid open-close-open sequences. The lifecycle owner should cancel or reverse animations cleanly rather than leaving two overlays or a permanently mounted closed dialog.
Where React ViewTransition fits
React also documents a ViewTransition reference for its newer view-transition integration (React ViewTransition reference). That API does not remove the fundamental lifecycle requirement for a dialog: the element or a framework-managed boundary must remain available while the transition is captured. Treat it as an additional transition mechanism under the conditions documented by React, not as a guarantee that an ordinary conditional unmount will animate.
The Tool Desk
Outbyte PC Repair FREEClear out junk files and repair common Windows errorsFree Scan →Outbyte Driver Updater FREEFix the driver behind crashes, sound loss and screen glitchesFind Drivers →The practical rule
Make the close operation a state change, not an immediate deletion. Keep the dialog primitive or animation boundary mounted, let exactly one lifecycle manager observe the leaving child, and remove the DOM only after the exit lifecycle completes. For Radix CSS animations, use its open and closed state attributes and verify the installed version’s Presence behavior. For JavaScript animation, use forceMount where Radix supports it and let the animation layer own final removal. For Motion, keep AnimatePresence outside the conditional and use stable direct-child keys. For React Transition Group, let the transition component manage unmountOnExit and match its timeout to the visual effect. Throughout, preserve focus, modality and screen-reader behavior until the dialog is genuinely closed.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

