Skip to content

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

Open
lunaxislu wants to merge 4 commits intoshadcn-ui:mainfrom
lunaxislu:update/base-ui-drawer-parity
Open

refactor(base-drawer): migrate base drawer to @base-ui/react/drawer#10430
lunaxislu wants to merge 4 commits intoshadcn-ui:mainfrom
lunaxislu:update/base-ui-drawer-parity

Conversation

@lunaxislu
Copy link
Copy Markdown

Supersedes #10292.

This PR migrates the base drawer from vaul to @base-ui/react/drawer and realigns both base and radix wrappers to provide the same abstraction where possible.

The previous attempt did not clearly separate wrapper replacement, direction layout ownership changes, and consumer migration into a single coherent flow. This PR restructures those boundaries so maintainers can review the migration in commit order.


What this does

Replaces the vaul-based base drawer with @base-ui/react/drawer, maintaining the same 10 public exports as the radix drawer. The goal is not to expose the primitive anatomy directly, but to ensure the shadcn wrapper provides the same abstraction across base and radix.

Export Base UI primitive Notes
Drawer DrawerPrimitive.Root
DrawerTrigger DrawerPrimitive.Trigger
DrawerPortal DrawerPrimitive.Portal
DrawerClose DrawerPrimitive.Close
DrawerOverlay DrawerPrimitive.Backdrop
DrawerContent Portal → Backdrop → Viewport → Popup → Content Internal composition
DrawerHeader <div>
DrawerFooter <div>
DrawerTitle DrawerPrimitive.Title
DrawerDescription DrawerPrimitive.Description

Base UI's drawer primitive exposes 13 parts; this wrapper surfaces 10 and removes DrawerPopup, DrawerViewport, and DrawerSwipeArea from the public API. DrawerContent now internally composes Portal → Backdrop → Viewport → Popup → Content, so consumers use the same level of wrapper abstraction as the radix drawer.

Why Viewport is kept internally: DrawerViewport was removed from the public API, but DrawerPrimitive.Viewport is retained in the internal composition. Base UI emits a dev warning when Popup renders without Viewport starting from v1.4.0, so this wrapper follows that constraint.


Direction layout ownership

The existing style files had cn-drawer-content with data-[vaul-drawer-direction=*] rules mixing layout (inset, position, width, max-height) and surface (rounded, border) together.

This was reasonable when vaul was the only primitive, but with Base UI added, putting data-[swipe-direction=*] into the same token turns shared CSS tokens into a primitive branching table rather than a style surface.

Comparing all 6 styles, the direction layout skeleton is largely identical across styles — what actually varies per style is surface (rounded-*, border-*, before:*).

Based on this analysis, ownership was reclassified:

Category Owner Examples
Direction layout Each primitive TSX inset-x-0, bottom-0, mt-24, max-h-[80vh], w-3/4, sm:max-w-sm
Gesture / transform mechanic Base UI TSX --drawer-bleed-*, --drawer-transform, negative margin, compensating padding
Theme-invariant behavior Each primitive TSX handle visibility, header alignment
Style surface Shared CSS token bg-*, rounded-*, border-*, before:*, typography

Principle: primitives own layout, styles own surface.

The same principle was applied to the radix drawer. Moving layout to TSX only for Base UI while leaving it in CSS for radix would create an asymmetry: "base owns layout in TSX, radix owns layout in CSS."


Shared token cleanup

Merged separate cn-base-ui-drawer-* / cn-radix-drawer-* tokens into cn-drawer-*, following the existing pattern where dialog/sheet already use shared cn-dialog-*/cn-sheet-* tokens across radix and base.

Post-merge token roles:

Token Content
cn-drawer-overlay bg-*, backdrop-filter (animation in each TSX)
cn-drawer-content bg-*, rounded-*, border-*, before:* (layout in each TSX)
cn-drawer-handle bg-*, size, rounded-full (direction visibility in each TSX)
cn-drawer-header gap-*, p-* (direction alignment in each TSX)
cn-drawer-title Unchanged
cn-drawer-description Unchanged
cn-drawer-footer Unchanged

Dead code trade-off

registry:build inlines shared CSS tokens into each base/style output as-is. Since it does not perform primitive-aware pruning, direction surface rules for the other primitive remain as dead code.

Example: data-[vaul-drawer-direction=bottom]:rounded-t-xl never matches in base drawer outputs.

This dead code is intentionally accepted:

  • rounded-*, border-* are theme-variable and cannot be moved to TSX
  • Dead code is limited to a small number of visual rules per direction
  • This is not unique to drawer — navigation-menu, dropdown-menu, and menubar already have shared tokens coexisting with primitive-specific selectors

In other words, this change prioritizes the ownership model — shared stylesheets own theme surface, primitive wrappers own direction layout — over uniformly eliminating dead code.

Direction: reduce layout dead code, accept surface dead code.


Visual verification

Opened base/radix drawers across all 6 styles and compared overlay and content using window.getComputedStyle().

Properties compared:

  • overlay: backgroundColor, backdropFilter, opacity
  • content: backgroundColor, color, borderRadius, borderTopWidth, borderTopStyle, position, top/right/bottom/left, maxHeight, width, fontSize, marginTop, marginBottom, padding

Overlay showed no differences across all 6 styles for background, blur, and opacity.

Content is not numerically identical due to Base UI's bleed mechanic, but the differences are concentrated in bleed compensation, and the focus was on confirming that the user-facing visual output remains equivalent.

Property Base UI Radix Cause
maxHeight 80vh + 3rem 80vh bleed compensation
width calc(100% + bleed) 100% bleed compensation
marginBottom -48px 0px hides bleed area
padding-bottom 48px 0px compensating padding

These differences stem from Base UI's swipe gesture bleed mechanic (--drawer-bleed-y: 3rem). The bleed area is hidden off-screen via negative margin, so the user-facing visual output remains equivalent to radix.


Parity verification

Checked base/radix parity:

Item Status
Public export count (base/radix match) 10/10
data-slot name symmetry Match
Shared CSS token coverage 7 cn-drawer-* tokens shared
Direction layout symmetry (TSX direct) base: swipe-direction, radix: vaul-drawer-direction
Direction surface symmetry (CSS token) dual selector (swipe-direction + vaul-drawer-direction)
Handle/header behavior symmetry Same behavior in each TSX
6 styles × base/radix visual parity Checked via computedStyle

API migration

Consumer changes (examples, blocks, app consumers, docs):

  • asChildrender
  • directionswipeDirection
  • top/bottomup/down
  • Docs: updated for Base UI, added Composition section

Commit structure

This PR is split into 4 commits by design decision order, not file type:

  1. refactor(base-drawer) — Base wrapper rewrite (2 files). Review the API surface here.
  2. refactor(drawer) — Direction rule ownership cleanup: moved layout and theme-invariant behavior from CSS to each primitive TSX (7 files). Review the ownership model here.
  3. fix(base-drawer) — Migrate all in-repo consumers (10 files). Mechanical API migration.
  4. chore — Regenerate registry outputs (41 files). Safe to skip.

Commits 1–2 contain the design decisions. Commit 3 is follow-through. Commit 4 is generated.

Note: Commit 2 intentionally completes the base/radix direction selector migration as a single ownership change. Keeping the stylesheet-to-primitive migration in one commit makes the layout/surface ownership model easier to review across both implementations. Since this PR will be squash-merged, I prioritized keeping that refactor readable as one coherent step.


Verification

  • pnpm --filter=v4 typecheck
  • pnpm --filter=v4 lint
  • pnpm --filter=v4 registry:build
  • pnpm --filter=v4 build
  • All 6 styles preserve visual parity between base and radix drawers
  • Manual verification confirms the migrated base drawer preserves the same visual result and interaction flow as the previous implementation
  • Manual verification confirms the radix drawer preserves the same visual result and interaction flow as before

…/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.
… 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-*.
…er 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.
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented Apr 18, 2026

@lunaxislu is attempting to deploy a commit to the shadcn-pro Team on Vercel.

A member of the Team first needs to authorize it.

@lunaxislu
Copy link
Copy Markdown
Author

If this PR is difficult to review in full right away, I’d appreciate confirmation on whether these two design decisions match the current direction of the repo:

  1. aligning the Base drawer public surface with the radix drawer surface
  2. letting the primitive wrappers own direction-specific layout, while keeping only style-dependent surface rules in the shared cn-drawer-* tokens

If those two directions look right, the rest of the changes are mostly migration work.

@lunaxislu
Copy link
Copy Markdown
Author

@shadcn Just following up on the two design questions above, in case it's easier to give a quick directional check before a full review.

I'd especially appreciate confirmation on:

  1. aligning the Base drawer public surface with the radix drawer surface
  2. letting the primitive wrappers own direction-specific layout, while keeping only style-dependent surface rules in the shared cn-drawer-* tokens

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant