headlessui icon indicating copy to clipboard operation
headlessui copied to clipboard

Disclosure defaultOpen transitions from closed to open causing wrong animation

Open jacogasp opened this issue 8 months ago • 2 comments

What package within Headless UI are you using?

@headlessui/react v2.2.2 tailwindcss v4.1

What browser are you using?

Chrome, Safari, and Firefox

Reproduction URL

https://codesandbox.io/p/sandbox/github/jacogasp/headless-ui-disclosure-animation

Describe your issue

Hello,

I'm using the Disclosure component to implement a simple drawer with animation provided by tailwindcss v4. The animation is handled by the data-[closed] attribute, e.g:

<Disclosure defaultOpen>
  <DisclosureButton>Open/Close</DisclosureButton>
  <DisclosurePanel
    as="aside"
    transition
    className=easy-in-out fixed inset-0 z-20 mt-16 w-80 duration-100 data-[closed]:-translate-x-full"
  >
    ...
  </DisclosurePanel>
<Dialog>

The problem is that, nevertheless the disclosure should be already open, the animation plays when landing on the page, probably because the component starts with the attribute data-closed reaching the data-open attribute after the initial render.

The expected behavior is that the animation does not play at all until I click the disclosure button to close the drawer.

jacogasp avatar Apr 23 '25 15:04 jacogasp

As dirty workaround I set a timeout to enable a boolean state which changes the className after some delay

e.g.

function Drawer() {
  const isMounted = useRef(false);
  const [animationEnabled, setAnimationEnabled] = useState(false);

  useEffect(() => {
    if (!isMounted.current) {
      setTimeout(() => setAnimationEnabled(true), 500);
      isMounted.current = true;
    }
  }, []);

  return (
    <Disclosure defaultOpen>
      <DisclosureButton>Open/Close</DisclosureButton>
      <DisclosurePanel
        as="aside"
        transition
        className={`fixed inset-0 z-20 mt-16 w-80 ${animationEnabled ? "ease-in-out duration-100 data-[closed]:-translate-x-full" : ""}`}
    >
      ...
      </DisclosurePanel>
    <Dialog>
  )
}

I tried the same trick toggling the transition={animationEnabled} attribute instead of altering the className string, but I got weird glitches.

jacogasp avatar Apr 23 '25 16:04 jacogasp

A much better approach is to remove the transition property and embed the <DisclosurePanel> inside a <Transition> component

<Disclosure defaultOpen>
  <DisclosureButton>Open/Close</DisclosureButton>
   <Transition>
    <DisclosurePanel
      as="aside"
      className="fixed inset-0 z-20 mt-16 w-80 ease-in-out duration-100 data-[closed]:-translate-x-full"
   >
      ...
    </DisclosurePanel>
  </Transition>
<Dialog>

jacogasp avatar Apr 24 '25 07:04 jacogasp

Your last solution is completely fine. Under the hood they use the same transition helpers and expose the same data attributes so your last code snippet works totally fine.

Why it works:

The <Transition> component by default doesn't transition on initial load, if you want that you can use the appear={true} prop (but you don't want this behavior).

If you use the transition prop, then we assume that you always want to transition. If you don't you can use transition={false}. The problem is that you do want to transition after the initial page load, so you could mimic that with:

+ import { useEffect, useState } from 'react'
  import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react'

  export default function Drawer() {
+   let [shouldTransition, setShouldTransition] = useState(false)
+   useEffect(() => setShouldTransition(true), [])

    return (
      <Disclosure defaultOpen>
        <header className="fixed inset-0 h-16 bg-slate-200">
          <DisclosureButton className="rounded bg-slate-400 p-2 hover:bg-slate-300">
            Open/Close
          </DisclosureButton>
          <DisclosurePanel
-           transition
+           transition={shouldTransition}
            className="duration-2000 fixed inset-0 mt-16 w-80 bg-slate-200 ease-in-out data-[closed]:-translate-x-full data-[closed]:bg-red-300"
          >
            Hello
          </DisclosurePanel>
        </header>
      </Disclosure>
    )
  }

But that's... a lot. So what you have is totally fine 👍

Going to close this issue since this is not really a bug, just different expectations around behavior.

RobinMalfait avatar Aug 29 '25 15:08 RobinMalfait