HeroUI Version
@heroui/react@3.0.1
Describe the bug
In HeroUI v3, the Drawer component cannot be opened programmatically without a trigger child element. The DrawerRoot wraps everything in React Aria's DialogTrigger, which expects its first child to be a trigger (Button) and the second child to be the overlay (Drawer.Backdrop). When no trigger child is present — which is the case for externally-controlled drawers — the Drawer.Backdrop is treated as the trigger instead of the overlay, so the drawer never opens.
This is a v2 to v3 migration regression. In v2, <Drawer isOpen={isOpen} onOpenChange={onOpenChange}> worked directly for controlled drawers without needing a trigger child. In v3, this pattern is broken.
What I tried (none work)
state prop with useOverlayState — <Drawer state={state}> where state = useOverlayState({ isOpen, onOpenChange }). The Drawer.Backdrop renders with display: none (has the hidden CSS class) and never becomes visible.
isOpen/onOpenChange on Drawer.Backdrop — Same result: backdrop stays hidden.
defaultOpen on Drawer root — <Drawer defaultOpen> — Same result.
useEffect calling state.open() — Opening via the state object after mount. Same result.
Root cause (from reading the source)
In drawer.tsx, DrawerRoot renders:
<DrawerTriggerPrimitive {...mergeProps(props, controlledProps)}>
{children}
</DrawerTriggerPrimitive>
Where DrawerTriggerPrimitive is DialogTrigger from react-aria-components. DialogTrigger requires its first child to be a pressable trigger element and its second child to be the overlay (ModalOverlay). Without a trigger as the first child, the overlay is never mounted as visible.
Use case
Many apps use drawers in a controlled, externally-driven way — for example, a detail panel that opens when a URL parameter changes, or when a list item is selected. In these cases:
- The parent component manages
isOpen state
-
- There is no "trigger button" — the drawer opens in response to external events (URL changes, row clicks, etc.)
-
-
- The parent conditionally renders the drawer or passes
isOpen={true} when it should be visible
This pattern was fully supported in v2 and is documented in the v3 migration guide under "Controlled State", but does not actually work in practice without a trigger child.
Steps to Reproduce the Bug or Issue
- Create a controlled Drawer without a trigger child — only
Drawer.Backdrop as a child of Drawer:
import { Drawer, useOverlayState } from "@heroui/react";
function MyPanel({ isOpen, onClose }) {
const state = useOverlayState({ isOpen, onOpenChange: (open) => { if (!open) onClose() } });
return (
<Drawer state={state}>
<Drawer.Backdrop>
<Drawer.Content placement="right">
<Drawer.Dialog>
<Drawer.Header><Drawer.Heading>Details</Drawer.Heading></Drawer.Header>
<Drawer.Body>Content here</Drawer.Body>
</Drawer.Dialog>
</Drawer.Content>
</Drawer.Backdrop>
</Drawer>
);
}
// Parent component:
function App() {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<button onClick={() => setIsOpen(true)}>Open</button>
<MyPanel isOpen={isOpen} onClose={() => setIsOpen(false)} />
</>
);
}
- Click the "Open" button to set
isOpen to true
-
- The Drawer renders in the DOM but
Drawer.Backdrop has display: none (the hidden CSS class is applied)
-
- The drawer never becomes visible
Also tried: defaultOpen, isOpen/onOpenChange directly on Drawer.Backdrop, wrapping in useEffect calling state.open() — none work.
Expected behavior
When using <Drawer state={state}> with useOverlayState({ isOpen: true }) (or isOpen/onOpenChange on Drawer.Backdrop), the Drawer should open and be visible — even without a trigger child element.
The v3 migration guide shows a "Controlled State" example using state prop with useOverlayState, but this only works if a Button trigger is present as the first child of Drawer. Apps that control drawers externally (e.g., via URL params or parent state) cannot use the trigger pattern.
Suggestion: Either allow Drawer.Backdrop to work as a standalone controlled overlay (like Modal.Backdrop seems intended to), or provide a way to use Drawer without the DialogTrigger wrapper when in fully controlled mode.
Screenshots or Videos
No response
Operating System Version
macOS
Browser
Chrome
HeroUI Version
@heroui/react@3.0.1
Describe the bug
In HeroUI v3, the
Drawercomponent cannot be opened programmatically without a trigger child element. TheDrawerRootwraps everything in React Aria'sDialogTrigger, which expects its first child to be a trigger (Button) and the second child to be the overlay (Drawer.Backdrop). When no trigger child is present — which is the case for externally-controlled drawers — theDrawer.Backdropis treated as the trigger instead of the overlay, so the drawer never opens.This is a v2 to v3 migration regression. In v2,
<Drawer isOpen={isOpen} onOpenChange={onOpenChange}>worked directly for controlled drawers without needing a trigger child. In v3, this pattern is broken.What I tried (none work)
stateprop withuseOverlayState—<Drawer state={state}>wherestate = useOverlayState({ isOpen, onOpenChange }). TheDrawer.Backdroprenders withdisplay: none(has thehiddenCSS class) and never becomes visible.isOpen/onOpenChangeonDrawer.Backdrop— Same result: backdrop stays hidden.defaultOpenonDrawerroot —<Drawer defaultOpen>— Same result.useEffectcallingstate.open()— Opening via the state object after mount. Same result.Root cause (from reading the source)
In
drawer.tsx,DrawerRootrenders:Where
DrawerTriggerPrimitiveisDialogTriggerfromreact-aria-components.DialogTriggerrequires its first child to be a pressable trigger element and its second child to be the overlay (ModalOverlay). Without a trigger as the first child, the overlay is never mounted as visible.Use case
Many apps use drawers in a controlled, externally-driven way — for example, a detail panel that opens when a URL parameter changes, or when a list item is selected. In these cases:
isOpenstateisOpen={true}when it should be visibleThis pattern was fully supported in v2 and is documented in the v3 migration guide under "Controlled State", but does not actually work in practice without a trigger child.
Steps to Reproduce the Bug or Issue
Drawer.Backdropas a child ofDrawer:isOpentotrueDrawer.Backdrophasdisplay: none(thehiddenCSS class is applied)Also tried:
defaultOpen,isOpen/onOpenChangedirectly onDrawer.Backdrop, wrapping inuseEffectcallingstate.open()— none work.Expected behavior
When using
<Drawer state={state}>withuseOverlayState({ isOpen: true })(orisOpen/onOpenChangeonDrawer.Backdrop), the Drawer should open and be visible — even without a trigger child element.The v3 migration guide shows a "Controlled State" example using
stateprop withuseOverlayState, but this only works if aButtontrigger is present as the first child ofDrawer. Apps that control drawers externally (e.g., via URL params or parent state) cannot use the trigger pattern.Suggestion: Either allow
Drawer.Backdropto work as a standalone controlled overlay (likeModal.Backdropseems intended to), or provide a way to useDrawerwithout theDialogTriggerwrapper when in fully controlled mode.Screenshots or Videos
No response
Operating System Version
macOS
Browser
Chrome