Breaking: @trackunit/react-drawer adds useDrawer, DrawerHeader, and variants
@trackunit/react-drawer has been reworked to align with Sheet and Modal: a new useDrawer hook owns state and dismiss, a new DrawerHeader component provides the standard toolbar, and the panel now has explicit variant="default" | "modal" semantics. Legacy inline props on <Drawer /> (open, onClose, onOpen, hasOverlay, keepMountedWhenClosed) and the DrawerToggle component have been removed.
New exports
useDrawer hook
Owns open/close state, dismiss handling, and Floating UI wiring for a Drawer. Consumers call useDrawer() and spread its return value onto <Drawer /> — the same shape as useSheet / useModal.
import { Drawer, DrawerHeader, useDrawer } from "@trackunit/react-drawer";
const drawer = useDrawer({ position: "right", variant: "modal" });
<>
<Button onClick={drawer.open}>Open</Button>
<Drawer {...drawer} ariaLabel="Filters">
<DrawerHeader onClickClose={drawer.close} />
<div className="p-4">…</div>
</Drawer>
</>
Supports controlled (isOpen) and uncontrolled (defaultOpen) modes, stable open / close / toggle / requestClose identities, and an onBeforeClose guard (sync or async — return false or a Promise<false> to keep the drawer open).
DrawerHeader component
Standard drawer toolbar with configurable affordances:
onClickClose— renders the built-in close (X) button.onClickBack/onClickForward— navigation arrows.menuContent— kebab overflow menu, typically a<MenuContent>with<MenuItem>children.hideCloseButton— for read-only drawers, or drawers whose parent already owns the close affordance.
Button titles come from the library's translation namespace and are not overridable — the affordances are universal ("Close", "Back", "Forward", "More actions").
<Drawer {...drawer}>
<DrawerHeader
menuContent={
<MenuContent>
<MenuItem id="edit" label="Edit" prefix={<Icon name="PencilSquare" size="small" />} />
</MenuContent>
}
onClickBack={goBack}
onClickClose={drawer.close}
/>
{/* body */}
</Drawer>
Wire the X to useDrawer's close so it shares the same dismiss pipeline (Escape, outside-press, onBeforeClose guard) as the rest of the drawer.
New types
UseDrawerPropsUseDrawerReturnValueDrawerVariant—"default" | "modal"DrawerDismissOptionsDrawerFloatingUiPropsDrawerHeaderProps
useDrawer reuses the shared overlay-dismissible types from @trackunit/react-components (UseOverlayDismissibleProps, OnCloseFn, OnBeforeCloseFn, DismissOptions, CloseReason) so the API matches useSheet and useModal.
New variant prop
Drawer now exposes a semantic variant that controls backdrop, focus trap, and dialog ARIA:
"default"— no backdrop, no focus trap,role="complementary". The surrounding page stays interactive. ESC still callsonClosewhen provided; outside-press does not close the drawer."modal"— dimming backdrop,role="dialog"+aria-modal, focus trap that returns focus to the trigger on close, and outside-press dismiss.
The focus trap on variant="modal" can be opted out with trapFocus: false for cases where a parent already manages focus for the drawer's subtree. trapFocus is ignored when variant="default" — the default variant never traps focus.
New accessibility props
Drawer now accepts either ariaLabel or ariaLabelledBy. Prefer ariaLabelledBy when the drawer body renders a visible heading so the visible label and the accessible name stay in sync.
<Drawer {...drawer} ariaLabelledBy="asset-inspector-title">
<h2 id="asset-inspector-title">{selectedAsset.name}</h2>
{/* … */}
</Drawer>
Without either prop, assistive technology has no accessible name for the panel and will announce it as an unnamed dialog/region.
Breaking changes
DrawerToggle removed
DrawerToggle is no longer exported from @trackunit/react-drawer. Call useDrawer's open / close / toggle from a trigger of your choice.
Before:
<DrawerToggle open={isOpen} onToggle={setIsOpen} />
After:
const drawer = useDrawer();
<Button onClick={drawer.toggle}>Toggle</Button>
<Drawer /> inline state props replaced by useDrawer return value
The following inline props on <Drawer /> have been removed:
openonCloseonOpenhasOverlaykeepMountedWhenClosedref
State and dismiss are now owned by useDrawer and spread onto the component.
Before:
const [isOpen, setIsOpen] = useState(false);
<Drawer hasOverlay onClose={() => setIsOpen(false)} open={isOpen} position="right">
{children}
</Drawer>
After:
const drawer = useDrawer({ position: "right", variant: "modal" });
<Drawer {...drawer}>
{children}
</Drawer>
The old hasOverlay toggle is now expressed via variant: variant="modal" renders the dim backdrop, variant="default" renders no backdrop and leaves the surrounding page interactive.
DrawerPosition narrowed to "left" / "right"
DrawerPosition no longer accepts "top" or "bottom". Use Sheet from @trackunit/react-components for bottom-anchored panels.
Panel width standardized
The drawer panel now renders at w-full sm:w-[28rem] max-w-[100dvw] by default. On mobile viewports the drawer fills the screen; on sm and wider it is 28 rem (448 px).
Consumers that need a different width should pass an sm:-or-wider-prefixed class via className. Unprefixed w-* classes are collapsed by twMerge and do not override the responsive base:
<Drawer {...drawer} className="sm:w-[720px]">
{children}
</Drawer>
Motion and dismiss now built on Floating UI
Modal drawers use @floating-ui/react for outside-press dismiss and focus-trap management (FloatingFocusManager). ESC and outside-press behaviour is configurable via useDrawer({ dismiss }), mirroring useSheet / useModal. Focus is moved into the panel after the enter transition settles, and the drawer no longer steals focus from a control the user has already interacted with during the slide-in.
Migration
Every drawer consumer in @trackunit/manager has been migrated in this release. External consumers should:
- Replace
open/onClosestate with auseDrawer()call and spread the result onto<Drawer />. - Choose a
variantexplicitly —"modal"matches the previoushasOverlaydefault;"default"matcheshasOverlay={false}. - If a close affordance was hand-rolled inside the drawer body, replace it with
<DrawerHeader onClickClose={drawer.close} />for the standard toolbar. - Drop any usage of
DrawerToggle; calldrawer.open/drawer.close/drawer.togglefrom a trigger of your choice. - Add an
ariaLabelorariaLabelledByto<Drawer />for an accessible name.
If the drawer wraps a form or content with unsaved changes, use onBeforeClose to guard the close:
const drawer = useDrawer({
onBeforeClose: async () => (await confirmDiscard()) === "discard",
});