Capítulo 47 de 108

Drawer

Core Idea

Painel deslizante (bottom sheet ou lateral) com suporte a swipe/gestos, snap points e drawers aninhados. Desde a migração recente, usa Base UI Drawer em vez de Vaul.

Key Concepts

  • swipeDirection: "up" | "right" | "down" | "left" — substitui o antigo direction do Vaul (bottomdown, topup).
  • showSwipeHandle: renderiza a alcinha visual de arrasto.
  • snapPoints: array de pontos de encaixe para drawers verticais; número entre 0-1 = fração da viewport, >1 = pixels, string aceita px/rem (ex. ["31rem", 1]).
  • snapPoint / onSnapPointChange: controle do ponto de encaixe ativo (substitui activeSnapPoint/setActiveSnapPoint do Vaul).
  • modal={false}: permite interagir com o resto da página com o drawer aberto; combine com disablePointerDismissal pra impedir fechar clicando fora; modal="trap-focus" mantém foco preso sem travar scroll/pointer.
  • Drawers aninhados: drawers-pai continuam montados e empilhados atrás do drawer mais recente aberto.
  • DrawerPortal, DrawerOverlay, DrawerSwipeHandle: exports de baixo nível para controle fino (além de DrawerContent, que já compõe portal+overlay+viewport+popup).

Code Examples

<Drawer
  open={open}
  onOpenChange={setOpen}
  showSwipeHandle={isMobile}
  swipeDirection={isMobile ? "down" : "right"}
>
  <DrawerTrigger render={<Button variant="secondary" />}>Open Drawer</DrawerTrigger>
  <DrawerContent>
    <DrawerHeader>
      <DrawerTitle>Pick a delivery time</DrawerTitle>
      <DrawerDescription>...</DrawerDescription>
    </DrawerHeader>
    <div className="flex-1 scroll-fade overflow-y-auto p-4">{/* conteúdo */}</div>
    <DrawerFooter>
      <Button onClick={handleConfirm}>Confirm Delivery Time</Button>
      <DrawerClose render={<Button variant="outline" />}>Cancel</DrawerClose>
    </DrawerFooter>
  </DrawerContent>
</Drawer>
  • O que demonstra: swipeDirection condicional por dispositivo (bottom sheet no mobile, side drawer no desktop) usando useIsMobile.
"use client"
import { useMediaQuery } from "@/hooks/use-media-query"

if (isDesktop) {
  return <Dialog>...</Dialog>
}
return <Drawer>...</Drawer>
  • O que demonstra: padrão "responsive dialog" — Dialog no desktop (min-width: 768px), Drawer no mobile, reaproveitando o mesmo form (ProfileForm).

Reference Tables

Composição:

Drawer
├── DrawerTrigger
└── DrawerContent
    ├── DrawerHeader
    │   ├── DrawerTitle
    │   └── DrawerDescription
    └── DrawerFooter

Variáveis CSS (setar em DrawerContent, exceto overlay):

VariableDefaultDescription
--drawer-inset0pxDistância do drawer às bordas do viewport
--drawer-bleed-backgroundvar(--color-popover)Preenche o vão atrás do drawer no overshoot do swipe
--drawer-overlay-min-opacity0Opacidade mínima do overlay (padrão 0.5 com snap points ativos)

Data attributes:

AttributeValuesQuando
data-swipe-directionup, right, down, leftSempre
data-swipe-axisx, ySempre
data-snap-pointspresenteDrawer tem snap points
data-expandedpresenteDrawer no snap point máximo
data-swipingpresenteSwipe em progresso
data-nested-drawer-openpresenteDrawer aninhado aberto por cima

Migração Vaul → Base UI (resumo):

VaulBase UI
npm install vaulnpm install @base-ui/react
direction="bottom"/"top"swipeDirection="down"/"up" (left/right iguais)
asChildrender={<Elemento />}
activeSnapPoint/setActiveSnapPointsnapPoint/onSnapPointChange
snapToSequentialPointsnapToSequentialPoints
onAnimationEndonOpenChangeComplete
onOpenAutoFocus={preventDefault}initialFocus={false}
dismissible={false}disablePointerDismissal
data-vaul-drawer-directiondata-swipe-direction
handleOnly, repositionInputs, shouldScaleBackgroundsem equivalente direto — usar modal, snapPoints, open controlado

Anti-patterns

  • Usar h-full numa região que deveria rolar: não resolve dentro de um drawer content-sized; use flex-1 overflow-y-auto na div de conteúdo, com header/footer fora dela.
  • Ficar em Vaul após migração da base: props Vaul-only (handleOnly etc.) não têm equivalente 1:1; recriar comportamento com props Base UI.

Key Takeaways

  1. Drawer é agora Base UI, não Vaul — migração exige trocar directionswipeDirection, asChildrender, e renomear props de snap point.
  2. snapPoints só se aplica a drawers verticais (up/down); aceita frações (0-1), pixels (>1) ou strings com unidade.
  3. Para região rolável dentro do drawer, usar flex-1 overflow-y-auto, nunca h-full.
  4. Drawers aninhados empilham automaticamente; cada nível fica montado atrás do topo via data-nested-drawer-open.
  5. Padrão "responsive dialog" (Dialog no desktop, Drawer no mobile) é a combinação oficial recomendada via useMediaQuery.
  6. iOS Safari exige body { position: relative; } nos estilos globais para o overlay cobrir a viewport corretamente após scroll.

Connects To

  • Dialog: par natural para o padrão responsivo (desktop=Dialog, mobile=Drawer).
  • Sheet: alternativa sem gestos de swipe/snap points, mais simples, para painéis laterais fixos.
  • Field / RadioGroup: usados no exemplo de seleção de horário de entrega dentro do drawer.