refactor(base-drawer): migrate base drawer to @base-ui/react/drawer (#10430)

* refactor(base-drawer): rewrite base drawer wrapper for @base-ui/react/drawer

Replace vaul with @base-ui/react/drawer as the primitive behind the
base drawer wrapper.

Keep the public API aligned with the radix drawer shape while
rewriting DrawerContent to compose Backdrop, Viewport, Popup, and
Content internally.

Keep the registry dependency change in the same commit so the wrapper
rewrite lands as one source-level migration step.

* refactor(drawer): move shared drawer direction rules from stylesheets to primitives

Shared drawer styles previously owned direction-specific layout in CSS.
With base and radix now exposing different direction attributes,
keeping that logic in shared tokens would duplicate primitive-specific
branching in the stylesheets.

Move direction-specific layout into the base/radix drawer wrappers and
leave shared CSS responsible only for visual surface styling.

Also move handle visibility and header alignment into TSX, and unify
the shared drawer token names to cn-drawer-*.

* fix(base-drawer): migrate examples, blocks, and app consumers to render and swipeDirection

Update in-repo base drawer consumers to the new wrapper API.

Replace asChild usage with render, switch direction to swipeDirection,
and align examples, blocks, app consumers, and docs with the new
base drawer usage pattern.

* chore: update registries

* fix(drawer): update base marker example usage

* fix(drawer): clean up base style selectors

* docs: add migration docs

* wip

* fix: nested

* feat: drawer

* fix

* fix

* fix

* fix: pointer events

* fix: clean up radix drawers

* fix: position

---------

Co-authored-by: shadcn <m@shadcn.com>
This commit is contained in:
coi
2026-07-02 19:16:37 +09:00
committed by GitHub
parent dbf9c5ebc4
commit 34223a842d
81 changed files with 2784 additions and 941 deletions

View File

@@ -4,14 +4,11 @@ description: A drawer component for React.
base: base
component: true
links:
doc: https://vaul.emilkowal.ski/getting-started
doc: https://base-ui.com/react/components/drawer
api: https://base-ui.com/react/components/drawer#api-reference
---
<ComponentPreview styleName="base-nova" name="drawer-demo" />
## About
Drawer is built on top of [Vaul](https://github.com/emilkowalski/vaul) by [emilkowalski](https://twitter.com/emilkowalski).
<ComponentPreview styleName="base-rhea" name="drawer-demo" />
## Installation
@@ -36,7 +33,7 @@ npx shadcn@latest add drawer
<Step>Install the following dependencies:</Step>
```bash
npm install vaul
npm install @base-ui/react
```
<Step>Copy and paste the following code into your project.</Step>
@@ -44,7 +41,7 @@ npm install vaul
<ComponentSource
name="drawer"
title="components/ui/drawer.tsx"
styleName="base-nova"
styleName="base-rhea"
/>
<Step>Update the import paths to match your project setup.</Step>
@@ -55,6 +52,14 @@ npm install vaul
</CodeTabs>
Add the following to your global styles. On iOS Safari, the drawer overlay is absolutely positioned and requires a positioned `body` to cover the viewport after the page is scrolled. See the [Base UI docs](https://base-ui.com/react/overview/quick-start#ios-26-safari) for details.
```css
body {
position: relative;
}
```
## Usage
```tsx showLineNumbers
@@ -72,17 +77,16 @@ import {
```tsx showLineNumbers
<Drawer>
<DrawerTrigger>Open</DrawerTrigger>
<DrawerTrigger render={<Button variant="outline" />}>Open</DrawerTrigger>
<DrawerContent>
<DrawerHeader>
<DrawerTitle>Are you absolutely sure?</DrawerTitle>
<DrawerDescription>This action cannot be undone.</DrawerDescription>
</DrawerHeader>
<div className="p-4">{/* Content here */}</div>
<DrawerFooter>
<Button>Submit</Button>
<DrawerClose>
<Button variant="outline">Cancel</Button>
</DrawerClose>
<DrawerClose render={<Button variant="outline" />}>Cancel</DrawerClose>
</DrawerFooter>
</DrawerContent>
</Drawer>
@@ -102,32 +106,215 @@ Drawer
└── DrawerFooter
```
`DrawerContent` composes the portal, overlay, viewport, and popup from Base UI. For lower-level control, `DrawerPortal`, `DrawerOverlay`, and `DrawerSwipeHandle` are also exported.
## Custom Sizes
A vertical drawer sizes itself to its content and is capped at `calc(100dvh - 6rem)` by default. A side drawer spans `75%` of the viewport width, or `24rem` on larger screens.
To customize the height of a vertical drawer, use the `h-*` and `max-h-*` utilities on `DrawerContent`.
```tsx
<DrawerContent className="h-[50vh]">
```
To customize the width of a side drawer, use the `w-*` and `max-w-*` utilities on `DrawerContent`.
```tsx
<DrawerContent className="w-96">
```
When the same component renders in multiple directions, scope an override to one axis using the `data-[swipe-axis=*]` variants.
```tsx
<DrawerContent className="data-[swipe-axis=y]:max-h-[50vh] data-[swipe-axis=x]:w-96">
```
To make a region of the drawer scrollable, make the scroll container a flex item. Avoid `h-full`, which does not resolve inside a content-sized drawer.
```tsx
<DrawerContent>
<DrawerHeader>...</DrawerHeader>
<div className="flex-1 overflow-y-auto p-4">{/* Scrollable content */}</div>
<DrawerFooter>...</DrawerFooter>
</DrawerContent>
```
## Styling
The drawer exposes CSS variables for style-level customization. Set the sizing variables on `DrawerContent`. Set the overlay variable on `[data-slot=drawer-overlay]` in your CSS.
| Variable | Default | Description |
| ------------------------------ | ---------------------- | ----------------------------------------------------------------------- |
| `--drawer-inset` | `0px` | Floats the drawer from the viewport edges. |
| `--drawer-bleed-background` | `var(--color-popover)` | Fills the gap behind the drawer on swipe overshoot. |
| `--drawer-overlay-min-opacity` | `0` | Minimum overlay opacity. Defaults to `0.5` when snap points are active. |
The drawer also sets data attributes you can target with variants such as `data-[swipe-direction=down]:` on `DrawerContent`, or `group-data-[swipe-axis=y]/drawer-popup:` on its descendants.
| Attribute | Values | Set when |
| ------------------------- | ----------------------------- | ------------------------------------- |
| `data-swipe-direction` | `up`, `right`, `down`, `left` | Always. |
| `data-swipe-axis` | `x`, `y` | Always. |
| `data-snap-points` | Present | The drawer has snap points. |
| `data-expanded` | Present | The drawer is at the full snap point. |
| `data-swiping` | Present | A swipe is in progress. |
| `data-nested-drawer-open` | Present | A nested drawer is open on top. |
## Examples
### Scrollable Content
### Position
Keep actions visible while the content scrolls.
Use the `swipeDirection` prop to set the side of the drawer.
<ComponentPreview styleName="base-nova" name="drawer-scrollable-content" />
Available options are `up`, `right`, `down`, and `left`.
### Sides
<ComponentPreview styleName="base-rhea" name="drawer-sides" />
Use the `direction` prop to set the side of the drawer. Available options are `top`, `right`, `bottom`, and `left`.
### Swipe Handle
<ComponentPreview styleName="base-nova" name="drawer-sides" />
Use `showSwipeHandle` on `Drawer` to render a swipe handle.
### Responsive Dialog
<ComponentPreview styleName="base-rhea" name="drawer-swipe-handle" />
### Nested
Open drawers from inside another drawer. Parent drawers stay mounted and stack behind the frontmost drawer.
<ComponentPreview styleName="base-rhea" name="drawer-nested" />
### Non Modal
Set `modal={false}` to allow interaction with the rest of the page while the drawer is open. Combine with `disablePointerDismissal` to prevent the drawer from closing on outside presses. Use `modal="trap-focus"` to keep focus inside the drawer while leaving scroll and pointer interaction unrestricted.
<ComponentPreview styleName="base-rhea" name="drawer-non-modal" />
### Snap Points
Use `snapPoints` to snap a drawer to preset heights. Numbers between `0` and `1` represent fractions of the viewport. Numbers greater than `1` are treated as pixel values. String values support `px` and `rem` units. Snap points apply to vertical drawers.
Track the active snap point with the controlled `snapPoint` and `onSnapPointChange` props. At the full snap point, the drawer gets a `data-expanded` attribute you can style with the `data-expanded:` variant.
<ComponentPreview styleName="base-rhea" name="drawer-snap-points" />
### Responsive
You can combine the `Dialog` and `Drawer` components to create a responsive dialog. This renders a `Dialog` component on desktop and a `Drawer` on mobile.
<ComponentPreview styleName="base-nova" name="drawer-dialog" />
<ComponentPreview styleName="base-rhea" name="drawer-dialog" />
## RTL
## Migrating from Vaul
To enable RTL support in shadcn/ui, see the [RTL configuration guide](/docs/rtl).
The base drawer now uses [Base UI](https://base-ui.com/react/components/drawer)
instead of Vaul. If you installed the previous base drawer, update your usage
to the Base UI API.
<ComponentPreview styleName="base-nova" name="drawer-rtl" direction="rtl" />
<Steps>
<Step>Update the dependency.</Step>
```diff
- npm install vaul
+ npm install @base-ui/react
```
<Step>Replace `direction` with `swipeDirection`.</Step>
Use `down` instead of `bottom`, and `up` instead of `top`. `left` and `right`
stay the same.
```diff
- <Drawer direction="bottom">
+ <Drawer swipeDirection="down">
```
<Step>Replace `asChild` with `render`.</Step>
For `DrawerTrigger`, pass the trigger element to the `render` prop.
```diff
- <DrawerTrigger asChild>
- <Button variant="outline">Open</Button>
- </DrawerTrigger>
+ <DrawerTrigger render={<Button variant="outline" />}>
+ Open
+ </DrawerTrigger>
```
For `DrawerClose`, pass the close element to the `render` prop.
```diff
- <DrawerClose asChild>
- <Button variant="outline">Cancel</Button>
- </DrawerClose>
+ <DrawerClose render={<Button variant="outline" />}>
+ Cancel
+ </DrawerClose>
```
<Step>Update snap point props.</Step>
If you use snap points, rename the controlled snap point props and the sequential
snap point prop.
```diff
<Drawer
snapPoints={[0.25, 0.5, 1]}
- activeSnapPoint={snapPoint}
- setActiveSnapPoint={setSnapPoint}
- snapToSequentialPoint
+ snapPoint={snapPoint}
+ onSnapPointChange={setSnapPoint}
+ snapToSequentialPoints
>
```
<Step>Update animation and focus props.</Step>
```diff
- <Drawer onAnimationEnd={(open) => setDone(open)}>
+ <Drawer onOpenChangeComplete={(open) => setDone(open)}>
```
```diff
- <DrawerContent onOpenAutoFocus={(event) => event.preventDefault()}>
+ <DrawerContent initialFocus={false}>
```
<Step>Review Vaul-only props.</Step>
Vaul props like `handleOnly`, `repositionInputs`, and
`shouldScaleBackground` do not have one-to-one replacements in the base drawer
API. Use Base UI props such as `disablePointerDismissal`, `modal`, `snapPoints`,
or controlled `open` state for the behavior you need.
```diff
- <Drawer handleOnly repositionInputs={false} shouldScaleBackground>
+ <Drawer>
```
```diff
- <Drawer dismissible={false}>
+ <Drawer disablePointerDismissal>
```
<Step>Update custom data attribute selectors.</Step>
Replace Vaul's `data-vaul-drawer-direction` selectors with Base UI's
`data-swipe-direction` selectors.
```diff
- <DrawerContent className="data-[vaul-drawer-direction=bottom]:max-h-[50vh]">
+ <DrawerContent className="data-[swipe-direction=down]:max-h-[50vh]">
```
Base UI also exposes attributes like `data-swiping`, `data-starting-style`, and
`data-ending-style` for swipe and transition states. Descendants inside
`DrawerContent` can use `group-data-[swipe-axis=x]/drawer-popup` and
`group-data-[swipe-axis=y]/drawer-popup` for axis-specific styling.
</Steps>
## API Reference
See the [Vaul documentation](https://vaul.emilkowal.ski/getting-started) for the full API reference.
See the [Base UI documentation](https://base-ui.com/react/components/drawer) for the full API reference.