#3892·headlessui

Dialog: interrupted enter transition deadlocks useTransition (open flip during duration-100 enter)

Author: mong-xCreated Sep 4, 2026Updated Sep 4, 2026

Summary

In @headlessui/react 2.2.x, a controlled Dialog with transition on DialogBackdrop / DialogPanel can deadlock when open flips from truefalse 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

typescript
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/react 2.2.9 / 2.2.10
  • React 19
  • Tailwind CSS v4 transition duration-100 + data-closed / data-enter classes