Repository: tailwindlabs/headlessui
Pull request #3309 (2024-06-20 19:18:08)

Add `transition` prop to `DialogPanel` and `DialogBackdrop` components
This PR is a continuation of #3307

In #3307 we introduced the `transition` prop on the `Dialog` component. But to make the API more consistent with the other components we want to be explicit and add the `transition` prop to the elements where the data attributes are being used.

This means that instead of enabling it on the `Dialog` directly, you add the `transition` prop to the individual components:

```tsx
<Dialog open={open} onClose={setOpen}>
  <DialogBackdrop transition className="ease-in-out duration-300 data-[closed]:opacity-0 data-[closed]:scale-95" />

  <DialogPanel transition className="ease-in-out duration-300 data-[closed]:opacity-0 data-[closed]:scale-95 bg-white">
    {/* … */}
  </DialogPanel>
</Dialog>
```

It's only needed on the `Dialog` component directly if you also want to transition this component.


vercel[bot]: [vc]: #pCeRWGbkLVErmR9Hb4k1nd2CrXQnXR/geOGMWQEwhnA=:eyJpc01vbm9yZXBvIjp0cnVlLCJ0eXBlIjoiZ2l0aHViIiwicHJvamVjdHMiOlt7Im5hbWUiOiJoZWFkbGVzc3VpLXZ1ZSIsInJvb3REaXJlY3RvcnkiOiJwbGF5Z3JvdW5kcy92dWUiLCJpbnNwZWN0b3JVcmwiOiJodHRwczovL3ZlcmNlbC5jb20vdGFpbHdpbmRsYWJzL2hlYWRsZXNzdWktdnVlLzVHUmI4cGJqZjJXalI5OGpBRjNLSjFtd0ZIUlQiLCJwcmV2aWV3VXJsIjoiaGVhZGxlc3N1aS12dWUtZ2l0LWZlYXQtdHJhbnNpdGlvbi1kaWFsb2ctYi0xZjM4OWEtdGFpbHdpbmRsYWJzLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiUEVORElORyIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJoZWFkbGVzc3VpLXZ1ZS1naXQtZmVhdC10cmFuc2l0aW9uLWRpYWxvZy1iLTFmMzg5YS10YWlsd2luZGxhYnMudmVyY2VsLmFwcCJ9fSx7Im5hbWUiOiJoZWFkbGVzc3VpLXJlYWN0Iiwicm9vdERpcmVjdG9yeSI6InBsYXlncm91bmRzL3JlYWN0IiwiaW5zcGVjdG9yVXJsIjoiaHR0cHM6Ly92ZXJjZWwuY29tL3RhaWx3aW5kbGFicy9oZWFkbGVzc3VpLXJlYWN0LzY2Wk5HWldVaUJ5TGN0VkpZRXYzY1ZMZ1pXRngiLCJwcmV2aWV3VXJsIjoiaGVhZGxlc3N1aS1yZWFjdC1naXQtZmVhdC10cmFuc2l0aW9uLWRpYWxvZy02ODJhZDYtdGFpbHdpbmRsYWJzLnZlcmNlbC5hcHAiLCJuZXh0Q29tbWl0U3RhdHVzIjoiUEVORElORyIsImxpdmVGZWVkYmFjayI6eyJyZXNvbHZlZCI6MCwidW5yZXNvbHZlZCI6MCwidG90YWwiOjAsImxpbmsiOiJoZWFkbGVzc3VpLXJlYWN0LWdpdC1mZWF0LXRyYW5zaXRpb24tZGlhbG9nLTY4MmFkNi10YWlsd2luZGxhYnMudmVyY2VsLmFwcCJ9fV19
**The latest updates on your projects**. Learn more about [Vercel for Git ↗︎](https://vercel.link/github-learn-more)

| Name | Status | Preview | Comments | Updated (UTC) |
| :--- | :----- | :------ | :------- | :------ |
| **headlessui-react** | 🔄 Building ([Inspect](https://vercel.com/tailwindlabs/headlessui-react/66ZNGZWUiByLctVJYEv3cVLgZWFx)) | [Visit Preview](https://vercel.live/open-feedback/headlessui-react-git-feat-transition-dialog-682ad6-tailwindlabs.vercel.app?via=pr-comment-visit-preview-link&passThrough=1) | 💬 [**Add feedback**](https://vercel.live/open-feedback/headlessui-react-git-feat-transition-dialog-682ad6-tailwindlabs.vercel.app?via=pr-comment-feedback-link) | Jun 20, 2024 7:18pm |
| **headlessui-vue** | 🔄 Building ([Inspect](https://vercel.com/tailwindlabs/headlessui-vue/5GRb8pbjf2WjR98jAF3KJ1mwFHRT)) | [Visit Preview](https://vercel.live/open-feedback/headlessui-vue-git-feat-transition-dialog-b-1f389a-tailwindlabs.vercel.app?via=pr-comment-visit-preview-link&passThrough=1) | 💬 [**Add feedback**](https://vercel.live/open-feedback/headlessui-vue-git-feat-transition-dialog-b-1f389a-tailwindlabs.vercel.app?via=pr-comment-feedback-link) | Jun 20, 2024 7:18pm |




Patch:
diff --git a/packages/@headlessui-react/CHANGELOG.md b/packages/@headlessui-react/CHANGELOG.md
index 6429626..e757863 100644
--- a/packages/@headlessui-react/CHANGELOG.md
+++ b/packages/@headlessui-react/CHANGELOG.md
@@ -11,7 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 
 - Add ability to render multiple `Dialog` components at once (without nesting them) ([#3242](https://github.com/tailwindlabs/headlessui/pull/3242))
 - Add CSS based transitions using `data-*` attributes ([#3273](https://github.com/tailwindlabs/headlessui/pull/3273), [#3285](https://github.com/tailwindlabs/headlessui/pull/3285))
-- Add `transition` prop to `Dialog` component ([#3307](https://github.com/tailwindlabs/headlessui/pull/3307))
+- Add `transition` prop to `Dialog`, `DialogBackdrop` and `DialogPanel` components ([#3307](https://github.com/tailwindlabs/headlessui/pull/3307), [#3309](https://github.com/tailwindlabs/headlessui/pull/3309))
 - Add `DialogBackdrop` component ([#3307](https://github.com/tailwindlabs/headlessui/pull/3307))
 - Add `PopoverBackdrop` component to replace `PopoverOverlay` ([#3308](https://github.com/tailwindlabs/headlessui/pull/3308))
 
diff --git a/packages/@headlessui-react/src/components/dialog/dialog.tsx b/packages/@headlessui-react/src/components/dialog/dialog.tsx
index 21b4d09..4bf680c 100644
--- a/packages/@headlessui-react/src/components/dialog/dialog.tsx
+++ b/packages/@headlessui-react/src/components/dialog/dialog.tsx
@@ -31,7 +31,7 @@ import { useServerHandoffComplete } from '../../hooks/use-server-handoff-complet
 import { useSyncRefs } from '../../hooks/use-sync-refs'
 import { CloseProvider } from '../../internal/close-provider'
 import { HoistFormFields } from '../../internal/form-fields'
-import { State, useOpenClosed } from '../../internal/open-closed'
+import { ResetOpenClosedProvider, State, useOpenClosed } from '../../internal/open-closed'
 import { ForcePortalRoot } from '../../internal/portal-force-root'
 import type { Props } from '../../types'
 import { match } from '../../utils/match'
@@ -52,8 +52,6 @@ import { FocusTrap, FocusTrapFeatures } from '../focus-trap/focus-trap'
 import { Portal, PortalGroup, useNestedPortals } from '../portal/portal'
 import { Transition, TransitionChild } from '../transition/transition'
 
-let WithTransitionWrapper = createContext(false)
-
 enum DialogStates {
   Open,
   Closed,
@@ -111,33 +109,9 @@ function stateReducer(state: StateDefinition, action: Actions) {
 
 // ---
 
-let DEFAULT_DIALOG_TAG = 'div' as const
-type DialogRenderPropArg = {
-  open: boolean
-}
-type DialogPropsWeControl = 'aria-describedby' | 'aria-labelledby' | 'aria-modal'
-
-let DialogRenderFeatures = RenderFeatures.RenderStrategy | RenderFeatures.Static
-
-export type DialogProps<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG> = Props<
-  TTag,
-  DialogRenderPropArg,
-  DialogPropsWeControl,
-  PropsForFeatures<typeof DialogRenderFeatures> & {
-    open?: boolean
-    onClose(value: boolean): void
-    initialFocus?: MutableRefObject<HTMLElement | null>
-    role?: 'dialog' | 'alertdialog'
-    autoFocus?: boolean
-    __demoMode?: boolean
-    transition?: boolean
-  }
->
-
-function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
-  props: DialogProps<TTag>,
-  ref: Ref<HTMLElement>
-) {
+let InternalDialog = forwardRefWithAs(function InternalDialog<
+  TTag extends ElementType = typeof DEFAULT_DIALOG_TAG,
+>(props: DialogProps<TTag>, ref: Ref<HTMLElement>) {
   let internalId = useId()
   let {
     id = `headlessui-dialog-${internalId}`,
@@ -146,7 +120,6 @@ function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
     initialFocus,
     role = 'dialog',
     autoFocus = true,
-    transition = false,
     __demoMode = false,
     ...theirProps
   } = props
@@ -179,39 +152,6 @@ function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
 
   let ownerDocument = useOwnerDocument(internalDialogRef)
 
-  // Validations
-  let hasOpen = props.hasOwnProperty('open') || usesOpenClosedState !== null
-  let hasOnClose = props.hasOwnProperty('onClose')
-  if (!hasOpen && !hasOnClose) {
-    throw new Error(
-      `You have to provide an \`open\` and an \`onClose\` prop to the \`Dialog\` component.`
-    )
-  }
-
-  if (!hasOpen) {
-    throw new Error(
-      `You provided an \`onClose\` prop to the \`Dialog\`, but forgot an \`open\` prop.`
-    )
-  }
-
-  if (!hasOnClose) {
-    throw new Error(
-      `You provided an \`open\` prop to the \`Dialog\`, but forgot an \`onClose\` prop.`
-    )
-  }
-
-  if (typeof open !== 'boolean') {
-    throw new Error(
-      `You provided an \`open\` prop to the \`Dialog\`, but the value is not a boolean. Received: ${open}`
-    )
-  }
-
-  if (typeof onClose !== 'function') {
-    throw new Error(
-      `You provided an \`onClose\` prop to the \`Dialog\`, but the value is not a function. Received: ${onClose}`
-    )
-  }
-
   let dialogState = open ? DialogStates.Open : DialogStates.Closed
 
   let [state, dispatch] = useReducer(stateReducer, {
@@ -343,19 +283,8 @@ function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
     }
   }
 
-  if (transition) {
-    let { transition: _transition, open, ...rest } = props
-    return (
-      <WithTransitionWrapper.Provider value={true}>
-        <Transition show={open}>
-          <Dialog ref={ref} {...rest} />
-        </Transition>
-      </WithTransitionWrapper.Provider>
-    )
-  }
-
   return (
-    <>
+    <ResetOpenClosedProvider>
       <ForcePortalRoot force={true}>
         <Portal>
           <DialogContext.Provider value={contextBag}>
@@ -391,8 +320,86 @@ function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
       <HoistFormFields>
         <MainTreeNode />
       </HoistFormFields>
-    </>
+    </ResetOpenClosedProvider>
   )
+})
+
+// ---
+
+let DEFAULT_DIALOG_TAG = 'div' as const
+type DialogRenderPropArg = {
+  open: boolean
+}
+type DialogPropsWeControl = 'aria-describedby' | 'aria-labelledby' | 'aria-modal'
+
+let DialogRenderFeatures = RenderFeatures.RenderStrategy | RenderFeatures.Static
+
+export type DialogProps<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG> = Props<
+  TTag,
+  DialogRenderPropArg,
+  DialogPropsWeControl,
+  PropsForFeatures<typeof DialogRenderFeatures> & {
+    open?: boolean
+    onClose(value: boolean): void
+    initialFocus?: MutableRefObject<HTMLElement | null>
+    role?: 'dialog' | 'alertdialog'
+    autoFocus?: boolean
+    transition?: boolean
+    __demoMode?: boolean
+  }
+>
+
+function DialogFn<TTag extends ElementType = typeof DEFAULT_DIALOG_TAG>(
+  props: DialogProps<TTag>,
+  ref: Ref<HTMLElement>
+) {
+  let { transition = false, open, ...rest } = props
+
+  // Validations
+  let usesOpenClosedState = useOpenClosed()
+  let hasOpen = props.hasOwnProperty('open') || usesOpenClosedState !== null
+  let hasOnClose = props.hasOwnProperty('onClose')
+
+  if (!hasOpen && !hasOnClose) {
+    throw new Error(
+      `You have to provide an \`open\` and an \`onClose\` prop to the \`Dialog\` component.`
+    )
+  }
+
+  if (!hasOpen) {
+    throw new Error(
+      `You provided an \`onClose\` prop to the \`Dialog\`, but forgot an \`open\` prop.`
+    )
+  }
+
+  if (!hasOnClose) {
+    throw new Error(
+      `You provided an \`open\` prop to the \`Dialog\`, but forgot an \`onClose\` prop.`
+    )
+  }
+
+  if (!usesOpenClosedState && typeof props.open !== 'boolean') {
+    throw new Error(
+      `You provided an \`open\` prop to the \`Dialog\`, but the value is not a boolean. Received: ${props.open}`
+    )
+  }
+
+  if (typeof props.onClose !== 'function') {
+    throw new Error(
+      `You provided an \`onClose\` prop to the \`Dialog\`, but the value is not a function. Received: ${props.onClose}`
+    )
+  }
+
+  let inTransitionComponent = usesOpenClosedState !== null
+  if (!inTransitionComponent && open !== undefined && !rest.static) {
+    return (
+      <Transition show={open} transition={transition} unmount={rest.unmount}>
+        <InternalDialog ref={ref} {...rest} />
+      </Transition>
+    )
+  }
+
+  return <InternalDialog ref={ref} open={open} {...rest} />
 }
 
 // ---
@@ -404,7 +411,9 @@ type PanelRenderPropArg = {
 
 export type DialogPanelProps<TTag extends ElementType = typeof DEFAULT_PANEL_TAG> = Props<
   TTag,
-  PanelRenderPropArg
+  PanelRenderPropArg,
+  never,
+  { transition?: boolean }
 >
 
 function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
@@ -412,7 +421,7 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
   ref: Ref<HTMLElement>
 ) {
   let internalId = useId()
-  let { id = `headlessui-dialog-panel-${internalId}`, ...theirProps } = props
+  let { id = `headlessui-dialog-panel-${internalId}`, transition = false, ...theirProps } = props
   let [{ dialogState }, state] = useDialogContext('Dialog.Panel')
   let panelRef = useSyncRefs(ref, state.panelRef)
 
@@ -433,20 +442,18 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
     onClick: handleClick,
   }
 
-  let Wrapper = useContext(WithTransitionWrapper) ? TransitionChild : Fragment
+  let Wrapper = transition ? TransitionChild : Fragment
 
   return (
-    <WithTransitionWrapper.Provider value={false}>
-      <Wrapper>
-        {render({
-          ourProps,
-          theirProps,
-          slot,
-          defaultTag: DEFAULT_PANEL_TAG,
-          name: 'Dialog.Panel',
-        })}
-      </Wrapper>
-    </WithTransitionWrapper.Provider>
+    <Wrapper>
+      {render({
+        ourProps,
+        theirProps,
+        slot,
+        defaultTag: DEFAULT_PANEL_TAG,
+        name: 'Dialog.Panel',
+      })}
+    </Wrapper>
   )
 }
 
@@ -459,14 +466,16 @@ type BackdropRenderPropArg = {
 
 export type DialogBackdropProps<TTag extends ElementType = typeof DEFAULT_BACKDROP_TAG> = Props<
   TTag,
-  BackdropRenderPropArg
+  BackdropRenderPropArg,
+  never,
+  { transition?: boolean }
 >
 
 function BackdropFn<TTag extends ElementType = typeof DEFAULT_BACKDROP_TAG>(
   props: DialogBackdropProps<TTag>,
   ref: Ref<HTMLElement>
 ) {
-  let theirProps = props
+  let { transition = false, ...theirProps } = props
   let [{ dialogState }] = useDialogContext('Dialog.Backdrop')
 
   let slot = useMemo(
@@ -476,7 +485,7 @@ function BackdropFn<TTag extends ElementType = typeof DEFAULT_BACKDROP_TAG>(
 
   let ourProps = { ref }
 
-  let Wrapper = useContext(WithTransitionWrapper) ? TransitionChild : Fragment
+  let Wrapper = transition ? TransitionChild : Fragment
 
   return (
     <Wrapper>
diff --git a/packages/@headlessui-react/src/components/disclosure/disclosure.tsx b/packages/@headlessui-react/src/components/disclosure/disclosure.tsx
index fa47e65..a173ff6 100644
--- a/packages/@headlessui-react/src/components/disclosure/disclosure.tsx
+++ b/packages/@headlessui-react/src/components/disclosure/disclosure.tsx
@@ -26,7 +26,12 @@ import { useResolveButtonType } from '../../hooks/use-resolve-button-type'
 import { optionalRef, useSyncRefs } from '../../hooks/use-sync-refs'
 import { useTransition, type TransitionData } from '../../hooks/use-transition'
 import { CloseProvider } from '../../internal/close-provider'
-import { OpenClosedProvider, State, useOpenClosed } from '../../internal/open-closed'
+import {
+  OpenClosedProvider,
+  ResetOpenClosedProvider,
+  State,
+  useOpenClosed,
+} from '../../internal/open-closed'
 import type { Props } from '../../types'
 import { isDisabledReactIssue7711 } from '../../utils/bugs'
 import { match } from '../../utils/match'
@@ -480,18 +485,20 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
   }
 
   return (
-    <DisclosurePanelContext.Provider value={state.panelId}>
-      {render({
-        mergeRefs,
-        ourProps,
-        theirProps,
-        slot,
-        defaultTag: DEFAULT_PANEL_TAG,
-        features: PanelRenderFeatures,
-        visible,
-        name: 'Disclosure.Panel',
-      })}
-    </DisclosurePanelContext.Provider>
+    <ResetOpenClosedProvider>
+      <DisclosurePanelContext.Provider value={state.panelId}>
+        {render({
+          mergeRefs,
+          ourProps,
+          theirProps,
+          slot,
+          defaultTag: DEFAULT_PANEL_TAG,
+          features: PanelRenderFeatures,
+          visible,
+          name: 'Disclosure.Panel',
+        })}
+      </DisclosurePanelContext.Provider>
+    </ResetOpenClosedProvider>
   )
 }
 
diff --git a/packages/@headlessui-react/src/components/popover/popover.tsx b/packages/@headlessui-react/src/components/popover/popover.tsx
index 1310c5d..3c99182 100644
--- a/packages/@headlessui-react/src/components/popover/popover.tsx
+++ b/packages/@headlessui-react/src/components/popover/popover.tsx
@@ -47,7 +47,12 @@ import {
   type AnchorProps,
 } from '../../internal/floating'
 import { Hidden, HiddenFeatures } from '../../internal/hidden'
-import { OpenClosedProvider, State, useOpenClosed } from '../../internal/open-closed'
+import {
+  OpenClosedProvider,
+  ResetOpenClosedProvider,
+  State,
+  useOpenClosed,
+} from '../../internal/open-closed'
 import type { Props } from '../../types'
 import { isDisabledReactIssue7711 } from '../../utils/bugs'
 import {
@@ -1043,44 +1048,46 @@ function PanelFn<TTag extends ElementType = typeof DEFAULT_PANEL_TAG>(
   })
 
   return (
-    <PopoverPanelContext.Provider value={id}>
-      <PopoverAPIContext.Provider value={{ close, isPortalled }}>
-        <Portal enabled={portal ? props.static || visible : false}>
-          {visible && isPortalled && (
-            <Hidden
-              id={beforePanelSentinelId}
-              ref={state.beforePanelSentinel}
-              features={HiddenFeatures.Focusable}
-              data-headlessui-focus-guard
-              as="button"
-              type="button"
-              onFocus={handleBeforeFocus}
-            />
-          )}
-          {render({
-            mergeRefs,
-            ourProps,
-            theirProps,
-            slot,
-            defaultTag: DEFAULT_PANEL_TAG,
-            features: PanelRenderFeatures,
-            visible,
-            name: 'Popover.Panel',
-          })}
-          {visible && isPortalled && (
-            <Hidden
-              id={afterPanelSentinelId}
-              ref={state.afterPanelSentinel}
-              features={HiddenFeatures.Focusable}
-              data-headlessui-focus-guard
-              as="button"
-              type="button"
-              onFocus={handleAfterFocus}
-            />
-          )}
-        </Portal>
-      </PopoverAPIContext.Provider>
-    </PopoverPanelContext.Provider>
+    <ResetOpenClosedProvider>
+      <PopoverPanelContext.Provider value={id}>
+        <PopoverAPIContext.Provider value={{ close, isPortalled }}>
+          <Portal enabled={portal ? props.static || visible : false}>
+            {visible && isPortalled && (
+              <Hidden
+                id={beforePanelSentinelId}
+                ref={state.beforePanelSentinel}
+                features={HiddenFeatures.Focusable}
+                data-headlessui-focus-guard
+                as="button"
+                type="button"
+                onFocus={handleBeforeFocus}
+              />
+            )}
+            {render({
+              mergeRefs,
+              ourProps,
+              theirProps,
+              slot,
+              defaultTag: DEFAULT_PANEL_TAG,
+              features: PanelRenderFeatures,
+              visible,
+              name: 'Popover.Panel',
+            })}
+            {visible && isPortalled && (
+              <Hidden
+                id={afterPanelSentinelId}
+                ref={state.afterPanelSentinel}
+                features={HiddenFeatures.Focusable}
+                data-headlessui-focus-guard
+                as="button"
+                type="button"
+                onFocus={handleAfterFocus}
+              />
+            )}
+          </Portal>
+        </PopoverAPIContext.Provider>
+      </PopoverPanelContext.Provider>
+    </ResetOpenClosedProvider>
   )
 }
 
diff --git a/packages/@headlessui-react/src/components/transition/transition.tsx b/packages/@headlessui-react/src/components/transition/transition.tsx
index 0a676d7..9a7281c 100644
--- a/packages/@headlessui-react/src/components/transition/transition.tsx
+++ b/packages/@headlessui-react/src/components/transition/transition.tsx
@@ -119,7 +119,7 @@ export type TransitionChildProps<TTag extends ReactTag> = Props<
   TransitionChildPropsWeControl,
   PropsForFeatures<typeof TransitionChildRenderFeatures> &
     TransitionClasses &
-    TransitionEvents & { appear?: boolean }
+    TransitionEvents & { transition?: boolean; appear?: boolean }
 >
 
 function useTransitionContext() {
@@ -298,6 +298,11 @@ function TransitionChildFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_
   ref: Ref<HTMLElement>
 ) {
   let {
+    // Wether or not to enable transitions on the current element (by exposing
+    // transition data). When set to false, the `Transition` component still
+    // acts as a transition boundary for `TransitionChild` components.
+    transition = true,
+
     // Event "handlers"
     beforeEnter,
     afterEnter,
@@ -402,15 +407,19 @@ function TransitionChildFn<TTag extends ElementType = typeof DEFAULT_TRANSITION_
   })
 
   useEffect(() => {
-    if (requiresRef) return
+    if (requiresRef && transition) return
 
     // When we don't transition, then we can complete the transition
     // immediately.
     start(show)
     end(show)
-  }, [show, requiresRef])
+  }, [show, requiresRef, transition])
 
   let enabled = (() => {
+    // Should the current component transition? If not, then we can still
+    // orchestrate the child transitions.
+    if (!transition) return false
+
     // If we don't require a ref, then we can't transition.
     if (!requiresRef) return false
 
diff --git a/packages/@headlessui-react/src/internal/open-closed.tsx b/packages/@headlessui-react/src/internal/open-closed.tsx
index b5ce312..2cb0f29 100644
--- a/packages/@headlessui-react/src/internal/open-closed.tsx
+++ b/packages/@headlessui-react/src/internal/open-closed.tsx
@@ -22,3 +22,7 @@ interface Props {
 export function OpenClosedProvider({ value, children }: Props): ReactElement {
   return <Context.Provider value={value}>{children}</Context.Provider>
 }
+
+export function ResetOpenClosedProvider({ children }: { children: React.ReactNode }): ReactElement {
+  return <Context.Provider value={null}>{children}</Context.Provider>
+}
diff --git a/playgrounds/react/pages/dialog/dialog-built-in-transition.tsx b/playgrounds/react/pages/dialog/dialog-built-in-transition.tsx
index a5de69e..d489b14 100644
--- a/playgrounds/react/pages/dialog/dialog-built-in-transition.tsx
+++ b/playgrounds/react/pages/dialog/dialog-built-in-transition.tsx
@@ -5,18 +5,28 @@ import { classNames } from '../../utils/class-names'
 
 export default function Home() {
   let [isOpen, setIsOpen] = useState(false)
-  let [transition, setTransition] = useState(true)
+  let [transitionBackdrop, setTransitionBackdrop] = useState(true)
+  let [transitionPanel, setTransitionPanel] = useState(true)
 
   return (
     <>
       <div className="flex gap-4 p-12">
         <Button onClick={() => setIsOpen((v) => !v)}>Toggle!</Button>
-        <Button onClick={() => setTransition((v) => !v)}>
-          <span>Toggle transition</span>
+        <Button onClick={() => setTransitionBackdrop((v) => !v)}>
+          <span>Toggle transition backdrop</span>
           <span
             className={classNames(
               'ml-2 inline-flex size-4 rounded-md',
-              transition ? 'bg-green-500' : 'bg-red-500'
+              transitionBackdrop ? 'bg-green-500' : 'bg-red-500'
+            )}
+          ></span>
+        </Button>
+        <Button onClick={() => setTransitionPanel((v) => !v)}>
+          <span>Toggle transition panel</span>
+          <span
+            className={classNames(
+              'ml-2 inline-flex size-4 rounded-md',
+              transitionPanel ? 'bg-green-500' : 'bg-red-500'
             )}
           ></span>
         </Button>
@@ -24,13 +34,18 @@ export default function Home() {
 
       <Dialog
         open={isOpen}
-        transition={transition}
         onClose={() => setIsOpen(false)}
-        className="relative z-50"
+        className="relative z-50 duration-500 data-[closed]:opacity-0"
       >
-        <DialogBackdrop className="fixed inset-0 bg-black/30 duration-500 ease-out data-[closed]:opacity-0" />
+        <DialogBackdrop
+          transition={transitionBackdrop}
+          className="fixed inset-0 bg-black/30 duration-500 ease-out data-[closed]:opacity-0"
+        />
         <div className="fixed inset-0 flex w-screen items-center justify-center p-4">
-          <DialogPanel className="w-full max-w-lg space-y-4 bg-white p-12 duration-500 ease-out data-[closed]:scale-95 data-[closed]:opacity-0">
+          <DialogPanel
+            transition={transitionPanel}
+            className="w-full max-w-lg space-y-4 bg-white p-12 duration-500 ease-out data-[closed]:scale-95 data-[closed]:opacity-0"
+          >
             <h1 className="text-2xl font-bold">Dialog</h1>
             <p>
               Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed pulvinar, nunc nec
diff --git a/playgrounds/react/pages/dialog/dialog.tsx b/playgrounds/react/pages/dialog/dialog.tsx
index 3ceb653..d3d3f29 100644
--- a/playgrounds/react/pages/dialog/dialog.tsx
+++ b/playgrounds/react/pages/dialog/dialog.tsx
@@ -80,17 +80,17 @@ export default function Home() {
               <Transition.Child
                 enter="ease-out duration-300"
                 enterFrom="opacity-0"
-                enterTo="opacity-75"
+                enterTo="opacity-100"
                 leave="ease-in duration-200"
-                leaveFrom="opacity-75"
+                leaveFrom="opacity-100"
                 leaveTo="opacity-0"
-                entered="opacity-75"
+                entered="opacity-100"
                 beforeEnter={() => console.log('[Transition.Child] [Overlay] Before enter')}
                 afterEnter={() => console.log('[Transition.Child] [Overlay] After enter')}
                 beforeLeave={() => console.log('[Transition.Child] [Overlay] Before leave')}
                 afterLeave={() => console.log('[Transition.Child] [Overlay] After leave')}
               >
-                <div className="fixed inset-0 bg-gray-500 transition-opacity" />
+                <div className="fixed inset-0 bg-gray-500/75 transition-opacity" />
               </Transition.Child>
 
               <Transition.Child


Test patch:
diff --git a/packages/@headlessui-react/src/components/dialog/dialog.test.tsx b/packages/@headlessui-react/src/components/dialog/dialog.test.tsx
index 99df8e6..2967dde 100644
--- a/packages/@headlessui-react/src/components/dialog/dialog.test.tsx
+++ b/packages/@headlessui-react/src/components/dialog/dialog.test.tsx
@@ -1019,41 +1019,6 @@ describe('Mouse interactions', () => {
     })
   )
 
-  it(
-    'should be possible to close the dialog, and keep focus on the focusable element',
-    suppressConsoleLogs(async () => {
-      function Example() {
-        let [isOpen, setIsOpen] = useState(false)
-        return (
-          <>
-            <button>Hello</button>
-            <button onClick={() => setIsOpen((v) => !v)}>Trigger</button>
-            <Dialog autoFocus={false} open={isOpen} onClose={setIsOpen}>
-              Contents
-              <TabSentinel />
-            </Dialog>
-          </>
-        )
-      }
-      render(<Example />)
-
-      // Open dialog
-      await click(getByText('Trigger'))
-
-      // Verify it is open
-      assertDialog({ state: DialogState.Visible })
-
-      // Click the button to close (outside click)
-      await click(getByText('Hello'))
-
-      // Verify it is closed
-      assertDialog({ state: DialogState.InvisibleUnmounted })
-
-      // Verify the button is focused
-      assertActiveElement(getByText('Hello'))
-    })
-  )
-
   it(
     'should be possible to submit a form inside a Dialog',
     suppressConsoleLogs(async () => {