Dialog: interrupted enter transition deadlocks useTransition (open flip during duration-100 enter)
Summary
In @headlessui/react 2.2.x, a controlled Dialog with transition on DialogBackdrop / DialogPanel can deadlock when open flips from true → false while the enter transition is still running (e.g. within ~50–100 ms of opening).
The panel stays mounted with data-enter data-closed and continues to intercept pointer events even though React's controlled open is already false.
Minimal repro
import { Dialog, DialogBackdrop, DialogPanel } from "@headlessui/react";
import { useEffect, useState } from "react";
export default function Repro() {
const [open, setOpen] = useState(false);
useEffect(() => {
setOpen(true);
const t = setTimeout(() => setOpen(false), 50); // during enter
return () => clearTimeout(t);
}, []);
return (
<Dialog open={open} onClose={setOpen} className="relative z-50">
<DialogBackdrop
transition
className="fixed inset-0 bg-black/40 transition duration-100 data-closed:opacity-0"
/>
<div className="fixed inset-0 flex items-center justify-center p-4">
<DialogPanel
transition
className="bg-white p-6 transition duration-100 data-closed:opacity-0 data-closed:scale-95"
>
Stuck panel if enter was interrupted
</DialogPanel>
</div>
</Dialog>
);
}Expected: after open becomes false, the panel unmounts (or finishes leave and unmounts).
Actual: panel remains mounted with data-enter data-closed and blocks clicks underneath.
Suspected cause
useTransition's leave cycle captures the enter's CSSTransitions via getAnimations() before the reversal cancels them. Their finished promises reject, so Promise.allSettled resolves immediately, and done() bails on inFlight && D(el) without rescheduling — leaving the element stuck mid-transition.
Workaround
Hold the controlled open prop at true for at least the enter duration (+ margin) before allowing it to go false (we use 300 ms for a duration-100 enter).
Environment
@headlessui/react2.2.9 / 2.2.10- React 19
- Tailwind CSS v4
transition duration-100+data-closed/data-enterclasses
Source: tailwindlabs/headlessui