A surface that slides in from an edge and swipes away, built on Base UI's drawer primitive.
import { Drawer } from '@oztix/roadie-components/drawer'
A bottom sheet. Drawer.Content collapses the underlying Portal, Backdrop, Viewport and Popup into one element, and adds the grab handle because the swipe axis is vertical.
Drawer.Content publishes --content-inset, so its header, body and footer share one horizontal inset. Override the variable on Drawer.Content to change all three.
side picks the edge, and with it the dismiss gesture. A bottom drawer swipes down. A right drawer swipes right. It is a single value, not a breakpoint list: Base UI owns the gesture in JavaScript, so the edge and the swipe have to agree.
emphasis on the root sets how much the drawer takes over the page behind it. normal dims and blurs the page, subtle tints it and leaves it readable, and subtler leaves it clear. A click outside still closes the drawer at any level. A small bottom or top drawer defaults to subtle, because it peeks over its page. Every other drawer defaults to normal.
size sets the drawer's extent along its own axis: height for a bottom or top drawer, width for a side one. sm, md and lg are fixed, so the drawer holds still while its content changes, and fit follows the content.
A bottom or top drawer measures its height against the space it can use. That space leaves out the far edge's safe area and a gap, so a strip of page always shows, like an iOS large sheet. In a wider window it also leaves out the float off the near edge.
| Size | Bottom or top drawer | Use for |
|---|---|---|
fit | The content's height, up to all of the space. The default | Short choices that don't change while open, such as a menu, a switcher or a confirmation |
sm | Half the space | A peek at something while the page stays in view. Its backdrop is subtle by default |
md | Three quarters of the space | A longer look that still keeps the page's top in view |
lg | All of the space | Tasks: multi-step flows, long lists with filters and forms |
A side drawer takes md by default, and fit there sizes to the content's width up to the lg width.
A bottom or top drawer runs edge to edge on a phone. In a wider window it stops at max-w-xl, centred, and floats 0.5rem off its edge with every corner rounded, the same shape as the cart drawer. Long content scrolls in Drawer.Body between a fixed header and footer.
The collapsed Drawer.Content covers the common case. Compose the parts directly when you need to reach the backdrop, the viewport or the popup separately.
<Drawer.Header><Drawer.Title>Organisations</Drawer.Title><Drawer.Description>Switch the organisation you're working in.</Drawer.Description></Drawer.Header>
Do
Start with Drawer.Header and a Drawer.Title. The title names the
dialog for assistive tech and tells people what they opened. Add a
Drawer.Description when the title needs context.
Don’t
A heading inside the body scrolls away and doesn't name the dialog. If the
content already has its own heading, pass aria-label to
Drawer.Content instead.
Do
Put a Drawer.Close inside Drawer.Header, rendered as a normal
IconButton named "Close". The header sets it in the top-left corner
above the title, where Close sits in a Pane and a Dialog.
If the header holds only a Close, keep it outside any wrapper with a
gap, such as a steps container. The wrapper's gap adds to the space
under the Close.
<Drawer.Closerender={<IconButton className='absolute top-4 right-4' … />}/>
Don’t
Don't position it yourself, move it to the other side or put it in the body. Close in the same corner everywhere is one less thing to look for.
Do
Give a Close to side drawers and to md and lg sheets: the tasks,
forms and long lists people work in. A swipe down isn't obvious to
everyone, and it isn't available with a mouse or keyboard.
Don’t
A fit sheet that closes when you pick something, such as a switcher or
a menu, doesn't need a Close. Picking, swiping, Escape and a click outside
all dismiss it.
<Drawer.Footer><Button intent='accent' emphasis='strong' className='w-full'>Share tickets</Button></Drawer.Footer>
Do
Put the action that completes the task in Drawer.Footer. It stays in
reach while the body scrolls, and on a phone a full-width button is the
easiest thing to hit.
<Drawer.Footer><Drawer.Close render={<Button>Close</Button>} /><Button intent='accent' emphasis='strong'>Share tickets</Button></Drawer.Footer>
Don’t
Don't repeat Close or Cancel in the footer when the header has one. Two ways out compete with the one way forward.
Do
Drawer.Body is the scroll region, and it tells the primitive that a drag
starting there is a scroll rather than a dismiss. Long content belongs
inside it.
<Drawer.Body><div className='overflow-y-auto'>{rows}</div></Drawer.Body>
Don’t
A second scroller inside the body competes with the swipe gesture, and the sheet stops tracking the thumb.
<Drawer side='bottom'><Drawer.Content>…</Drawer.Content></Drawer>
Do
Drawer.Content places it for you. On a bottom sheet it reads as the grab
affordance iOS users reach for. It is off by default where the swipe
axis is horizontal.
<Drawer side='right'><Drawer.Content handle>…</Drawer.Content></Drawer>
Don’t
A horizontal swipe has no matching affordance in the platform, and a stray pill at the top of a side drawer reads as decoration.
<Drawer.Content size='sm'>{/* the event stays readable behind the tickets */}</Drawer.Content>
Do
Leave emphasis to its default. A small sheet over a page the user is
still reading keeps that page legible, and a larger one takes their focus.
<Drawer.Backdrop className='bg-[oklch(0.1_0.04_250/0.3)]' />
Don’t
Restating the scrim's colour to lighten it drifts from Roadie's the moment
it changes. Set emphasis on the root instead.
<Drawer side='right'><Drawer.Content>…</Drawer.Content></Drawer>
Do
The root sets the edge and the dismiss gesture together, and every part
reads it from there. No part takes a side of its own, so the two can
never disagree.
Don’t
side is a single value, not a breakpoint list. Base UI owns the gesture
in JavaScript. Pick it from your own media query in application code and
pass the result.
Escape handling and the role='dialog' wiring.Drawer.Title and Drawer.Description supply the accessible name and description. A drawer without a title has no name.Drawer.Handle is aria-hidden. The gesture it hints at is not the only way to dismiss, so it carries no semantics of its own.prefers-reduced-motion: reduce.The content of the drawer.
A handle to associate the drawer with a trigger. If specified, allows detached triggers to control the drawer's open state. Can be created with the Drawer.createHandle() method.
Whether the drawer is currently open.
Whether the drawer is initially open. To render a controlled drawer, use the `open` prop instead.
Defaults to false.
Determines if the drawer enters a modal state when open. - `true`: user interaction is limited to just the drawer: focus is trapped, document page scroll is locked, and pointer interactions on outside elements are disabled. - `false`: user interaction with the rest of the document is allowed. - `'trap-focus'`: focus is trapped inside the drawer, but document page scroll is not locked and pointer interactions outside of it remain enabled.
Defaults to true.
Event handler called when the drawer is opened or closed.
Event handler called after any animations complete when the drawer is opened or closed.
Whether to prevent the drawer from closing on outside presses. For non-modal drawers, this also prevents the drawer from closing when focus moves outside of it.
Defaults to false.
A ref to imperative actions. - `unmount`: Manually unmounts the drawer. Call this after any externally controlled closing animation finishes. - `close`: Closes the drawer imperatively when called.
ID of the trigger that the drawer is associated with. This is useful in conjunction with the `open` prop to create a controlled drawer. There's no need to specify this prop when the drawer is uncontrolled (that is, when the `open` prop is not set).
ID of the trigger that the drawer is associated with. This is useful in conjunction with the `defaultOpen` prop to create an initially open drawer.
Snap points used to position the drawer. Use numbers between 0 and 1 to represent fractions of the viewport height, numbers greater than 1 as pixel values, or strings in `px`/`rem` units (for example, `'148px'` or `'30rem'`).
Disables velocity-based snap skipping so drag distance determines the next snap point.
Defaults to false.
The currently active snap point. Use with `onSnapPointChange` to control the snap point.
The initial snap point value when uncontrolled.
Callback fired when the snap point changes.
The edge the drawer is anchored to, which also sets the swipe direction.
Defaults to 'bottom'.
How much the drawer takes over the page behind it. A small bottom or top drawer defaults to `subtle`, because it peeks over its page; every other drawer defaults to `normal`.
CSS class applied to the element, or a function that returns a class based on the component's state.
Inherited from DrawerBackdropProps
Whether the backdrop is forced to render even when nested.
Defaults to false.
No additional props. It forwards all standard HTML attributes to the underlying element.
CSS class applied to the element, or a function that returns a class based on the component's state.
Inherited from NativeButtonProps
Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`).
Defaults to true.
CSS class applied to the element, or a function that returns a class based on the component's state.
The colour palette; inherited when unset.
Height for a bottom or top drawer, width for a side one. `sm`, `md` and `lg` are fixed; `fit` follows the content. Defaults to `fit` on the top or bottom, `md` on a side.
Renders the grab handle; on by default for bottom and top drawers.
Inherited from DrawerPopupProps
Determines the element to focus when the drawer is opened. - `false`: Do not move focus. - `true`: Move focus based on the default behavior (first tabbable element or popup). - `RefObject`: Move focus to the ref element. - `function`: Called with the interaction type (`mouse`, `touch`, `pen`, or `keyboard`). Return an element to focus, `true` to use the default behavior, or `false`/`undefined` to do nothing.
Determines the element to focus when the drawer is closed. - `false`: Do not move focus. - `true`: Move focus based on the default behavior (trigger or previously focused element). - `RefObject`: Move focus to the ref element. - `function`: Called with the interaction type (`mouse`, `touch`, `pen`, or `keyboard`). Return an element to focus, `true` to use the default behavior, or `false`/`undefined` to do nothing.
No additional props. It forwards all standard HTML attributes to the underlying element.
No additional props. It forwards all standard HTML attributes to the underlying element.
No additional props. It forwards all standard HTML attributes to the underlying element.
No additional props. It forwards all standard HTML attributes to the underlying element.
CSS class applied to the element, or a function that returns a class based on the component's state.
The colour palette; inherited when unset.
Height for a bottom or top drawer, width for a side one. `sm`, `md` and `lg` are fixed; `fit` follows the content. Defaults to `fit` on the top or bottom, `md` on a side.
Inherited from DrawerPopupProps
Determines the element to focus when the drawer is opened. - `false`: Do not move focus. - `true`: Move focus based on the default behavior (first tabbable element or popup). - `RefObject`: Move focus to the ref element. - `function`: Called with the interaction type (`mouse`, `touch`, `pen`, or `keyboard`). Return an element to focus, `true` to use the default behavior, or `false`/`undefined` to do nothing.
Determines the element to focus when the drawer is closed. - `false`: Do not move focus. - `true`: Move focus based on the default behavior (trigger or previously focused element). - `RefObject`: Move focus to the ref element. - `function`: Called with the interaction type (`mouse`, `touch`, `pen`, or `keyboard`). Return an element to focus, `true` to use the default behavior, or `false`/`undefined` to do nothing.
CSS class applied to the element, or a function that returns a class based on the component's state.
Inherited from DrawerPortalProps
Whether to keep the portal mounted in the DOM while the popup is hidden.
Defaults to false.
A parent element to render the portal element into.
An unpositioned, invisible strip that opens the drawer on a swipe.
CSS class applied to the element, or a function that returns a class based on the component's state.
Inherited from DrawerSwipeAreaProps
Whether the swipe area is disabled.
Defaults to false.
The swipe direction that opens the drawer. Defaults to the opposite of `Drawer.Root` `swipeDirection`.
No additional props. It forwards all standard HTML attributes to the underlying element.
CSS class applied to the element, or a function that returns a class based on the component's state.
Inherited from DrawerTriggerProps
A handle to associate the trigger with a drawer. Can be created with the Drawer.createHandle() method.
A payload to pass to the drawer when it is opened.
ID of the trigger. In addition to being forwarded to the rendered element, it is also used to specify the active trigger for drawers in controlled mode (with the Drawer.Root `triggerId` prop).
Inherited from NativeButtonProps
Whether the component renders a native `<button>` element when replacing it via the `render` prop. Set to `false` if the rendered element is not a button (for example, `<div>`).
Defaults to true.
No additional props. It forwards all standard HTML attributes to the underlying element.