Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
Components
Buttons & Actions
ButtonButton GroupCopy ButtonDelete ButtonTheme ToggleToggleToggle Group
Inputs
Avatar UploadCalendarCheckboxColor PickerComboboxCountry SelectCron PickerCurrency InputDate PickerDate Range PickerDate Time PickerDuration PickerEmail InputEmoji PickerFile UploadFilter BuilderInputInput GroupListboxMasked InputMention InputMonth PickerMulti SelectNumber InputOTP InputPassword InputPhone InputRadio GroupRatingSearch InputSegmented ControlSelectSignature PadSliderSwitchTag InputTextareaTime PickerTimezone SelectTree Select
Forms
Address InputBooking WidgetCSV ImportFieldForm WizardInline EditInvoice line itemsRich Text EditorSchema Builder
Navigation
Back To TopBounce SidebarBreadcrumbCommand MenuGlobal SearchGooey NavHook SidebarNav MenuNav TabsPaginationProximity SidebarScroll ProgressSidebarStepsTable Of ContentsTabsTopbarUser MenuWorkspace Switcher
Overlays
Alert DialogContext MenuDialogDrawerDropdown MenuFamily DrawerHover CardPopoverShare DialogSheetShortcuts DialogTooltipTour
Feedback
AlertBannerCookie ConsentEmoji ReactionEmpty StateNotification BellNotification CenterProgressRoute ProgressSetup ChecklistSkeletonSpinnerToast
Data Display
Approval FlowAvatarBadgeCalendar ViewCode BlockComment ThreadContact cardCountdownDescription ListDiff ViewerFile ListFile managerFolderGanttJSON ViewerKbdLog ViewerMarkdownPresenceSchedulerTagTimelineTree View
Tables & Lists
Audit LogData TableDeal PipelineEditable GridKanbanListMembers tablePermissions MatrixTableTask ListVirtual List
Charts & Stats
Animated CounterArea chartBar chartDonut chartFunnel chartGaugeGitHub ActivityHeatmap calendarLeaderboardLine chartRadar chartRadial barSparklineStat CardTrackerTrend badge
Layout
AccordionApp ShellCardCollapsibleDetail LayoutEntity HeaderPage HeaderScroll AreaSeparatorSettings LayoutSplit ViewToolbar
Media
Audio PlayerCarouselCSV ViewerDOCX EditorDOCX ViewerExcel ViewerFile SystemImage compareImage CropperImage GalleryImage LightboxLocation MapPDF ViewerPowerPoint ViewerQR CodeStep PlayerVideo Player
AI Kit
Agent Status CardChat MessageChat ThreadChat WidgetConversation ListFluid OrbGrid RevealMatrix OrbModel PickerPrompt InputReasoning IndicatorRule BuilderSuggestion ChipsTool Call TimelineUsage MeterVoice InputWorkflow Canvas
Text & Effects
Background PatternGravity LettersMarqueeTerminalText AnimateText Rotate
Mobile
Action SheetBottom Tab BarFABHaptic ButtonHero HeaderInstall PromptLarge Title HeaderNotification BannerOnboarding CarouselPicker WheelPull To RefreshSlide To ConfirmSticky CTA BarSwipe DeckSwipeable Tabs
Commerce
Cart drawerCategory pickerCheckout stepsCoupon inputFavorite buttonFilter panelListing cardListing gridOrder trackerPricePrice rangePricing tableProduct galleryQuantity stepperReviewsSeller card
Marketing Sections
App Download SectionBlog Cards SectionCTA SectionFAQ SectionFeature GridFeature ShowcaseHero SectionIntegrations SectionLogo CloudMarketing FooterMarketing HeaderMarketing SectionNewsletter SectionPricing SectionStats SectionSteps SectionTeam SectionTestimonials SectionTimeline SectionVideo Section
Blocks
Admin Dashboard PageAdmin ShellAI Chat PageAPI Keys PageAutomation Builder PageCheckout PageCRM Contact PageCRM Pipeline PageError PagesInbox PageInvoice PageKPI DashboardListing Detail PageLogin PageMarketplace Home PageMarketplace Search PageMarketplace ShellMobile App HomeOnboarding FlowOrder History PagePost Listing WizardPricing PageProfile PageRegister PageSettings PageUsers Admin Page
Templates
About PageAcademy PageAccessibility PageAffiliate PageAgency PageAI Product PageAppointment Booking PageBlog PageBlog Post PageBug Bounty PageCareers PageChangelog PageCommunity PageCompare PageContact PageCustomer Story PageCustomers PageDemo Booking PageDevelopers PageDocs Home PageDownload PageEnterprise PageEvents PageExperts PageFAQ PageFeature PageGlossary PageHelp Article PageHelp Center PageHome Services PageImpact PageIntegration Detail PageIntegrations PageJob Detail PageLanding PageLaunch PageLegal PageManifesto PagePortfolio PagePress PageProduct Tour PagePromo PageReal Estate PageReferral PageResearch Report PageRestaurant PageRoadmap PageROI Calculator PageSecurity PageSolutions PageStartups PageStatus PageStorefront PageSwitch PageTemplate Gallery PageTravel PageWaitlist PageYear in Review Page
Hooks & Utilities
AI ChatChart kitCommerceDate utilsFile exportFile typesFiltersForm validationHapticsInvoicingMotion presetsNavigationNumber formatRelative timeRomanian locationsSpreadsheetText matchTonesUIProvideruseClipboarduseConsentuseControllableStateuseCooldownuseDebounceuseKeyboardInsetuseLocaleuseLongPressuseMediaControlsuseMediaQueryuseOverflowItemsusePersistedStateuseScrollEdgesuseScrollStateuseSelectionuseShortcutuseStickToBottom
GalleryTheme

Overlays

Drawer

Overlays

Drawer React component for Tailwind CSS v4

DialogDropdown Menu
Live props
State
defaultOpen
Content
title
description
hideTitle
closeLabel
Layout
direction
desktop
size
height
Appearance
variant
radius
showHandle
showClose
overlay
headerAlign
Behavior
dismissible
modal
handleOnly
scaleBackground
repositionInputs
shortcut
Code
import { Drawer } from "@/components/ui/drawer"

<Drawer />
  1. Home
  2. Components
  3. Overlays
  4. Drawer
Free

Drawer React component for Tailwind CSS v4

v1.0.0·Added in 1.0.0

A React drawer and bottom sheet built on vaul: drag to dismiss, snap points, nesting, and a side panel or dialog on desktop.

Tap the trigger and the sheet slides up from the bottom on the iOS sheet curve while the page dims. Drag it down by the handle or anywhere on it and it follows your finger; let go past a quarter of its height, or flick it, and it closes, otherwise it settles back. A long list inside scrolls first and only drags the sheet once it is back at the top; the header hairline appears as soon as the list scrolls and the footer hairline while there is more below. Snap points stop it half open and a tap on the handle steps to the next point. A drawer opened from inside another one pushes the first one back, like iOS. Escape, a tap on the dim page or the close button close it, unless it is not dismissible. Opening moves focus to the sheet itself (no field pops the phone keyboard, Tab goes on to the first control; the desktop dialog focuses its first text field, or itself when there is none), keeps it inside while open and returns it to the trigger after. A shortcut, when set, toggles it from anywhere. From 768px it stays a centered sheet, or becomes a side panel from the right or a centered dialog, whichever desktop asks for. With reduced motion the sheet's slide ends in 200ms and the dialog only fades.

Drawer is a free React component in the React overlay components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add drawer, together with Button, Motion presets, useControllableState, useMediaQuery, useScrollEdges and 2 more and the npm packages @radix-ui/react-dialog, @radix-ui/react-visually-hidden, lucide-react, motion and vaul.

Add to a project

Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup

npx wingo-ui@latest add drawer

Usage

Import Drawer from @/components/ui/drawer.

import { SlidersHorizontal } from "lucide-react"
import { Button } from "@/components/ui/button"
import { Drawer, DrawerClose } from "@/components/ui/drawer"

export function ListingFilters({ results, onReset }: { results: number; onReset: () => void }) {
  return (
    <Drawer
      trigger={
        <Button variant="soft" leftIcon={<SlidersHorizontal />}>
          Filters
        </Button>
      }
      title="Filters"
      description="New York · 1,240 listings"
      desktop="side"
      closeLabel="Close filters"
      shortcut="mod+j"
      footer={
        <>
          <DrawerClose asChild>
            <Button size="lg" fullWidth>
              Show {results} results
            </Button>
          </DrawerClose>
          <Button size="lg" variant="ghost" fullWidth onClick={onReset}>
            Reset
          </Button>
        </>
      }
    >
      <p className="text-sm text-muted-foreground">Price, condition and categories</p>
    </Drawer>
  )
}

Props

  • defaultOpendefault false
    boolean

    Starts open (uncontrolled) and skips the entrance animation. Pass open and onOpenChange instead to control it.

  • open
    boolean

    Controlled open state. The root keeps it across the 768px breakpoint, so a resize never closes the drawer.

  • onOpenChange
    (open: boolean) => void

    Called when the drawer opens or closes, by the trigger, a drag, Escape, an outside tap or a close button.

  • titledefault ""
    ReactNode

    The title under the handle, also the dialog's accessible name. The demo shows its sample while this is empty.

  • descriptiondefault ""
    ReactNode

    A quieter line under the title, linked with aria-describedby. Without one the drawer opts out of the description (Radix's aria-describedby={undefined}).

  • hideTitledefault false
    boolean

    Keeps the title for screen readers only, for sheets whose content speaks for itself (a share sheet, a photo).

  • closeLabeldefault "Close"
    string

    aria-label of the close button, e.g. “Close filters”. The demo spells it out under the page.

  • trigger
    ReactNode

    The element that opens the drawer, rendered with asChild: a single element that takes a ref, like a Button. It gets aria-keyshortcuts when a shortcut is set.

  • children
    ReactNode

    The body. It scrolls on its own (overscroll contained), pads the home indicator when it is the last part, and drives the header and footer hairlines.

  • footer
    ReactNode

    Actions pinned under the body, primary first: stacked full width in sheets (primary on top), a row with the primary on the right in side and dialog modes. Give sheet buttons size="lg" fullWidth.

  • directiondefault "bottom"
    "bottom" | "top" | "left" | "right"

    The edge it slides in from. Bottom and top sheets get the handle; left and right panels are 320 to 540px wide, always show the close button, and leave a tappable sliver of the page on phones.

  • desktopdefault "sheet"
    "sheet" | "side" | "dialog"

    What it becomes at 768px and up. sheet: the same drawer, capped and centered. side: a panel floating 8px off the right edge. dialog: a centered dialog with a spring (no drag). Phones always get the sheet.

  • sizedefault "md"
    "sm" | "md" | "lg" | "full"

    Bottom and top sheets on desktop: max width 420 / 560 / 720px or the whole screen. Left, right and side panels: 320 / 400 / 540px or the whole screen. Dialog: 400 / 520 / 720px or 16px from every edge.

  • heightdefault "auto"
    "auto" | "full"

    auto grows with the content up to 92% of the screen; full always takes 92%. Sheets with snap points are full.

  • variantdefault "attached"
    "attached" | "floating"

    attached sits on the screen edge with the outer corners square; floating keeps 8px off every edge and the safe area, with every corner rounded (the iOS 26 look). The desktop side panel always floats.

  • radiusdefault "lg"
    "none" | "sm" | "md" | "lg"

    Sheet corners 0 / 16 / 24 / 28px (28px is the design system's sheet radius). In dialog mode 0 / 12 / 16 / 24px. Any value through className="[--drawer-radius:20px]".

  • showHandledefault true
    boolean

    The drag handle of bottom and top sheets. A tap on it steps through the snap points. A sheet that is not dismissible and has no snap points hides it, since there is nothing to drag (like iOS).

  • showClosedefault false
    boolean

    Adds the round close button to the corner of bottom and top sheets. Panels and dialogs always have one, since they have no handle.

  • overlaydefault true
    boolean

    The dim backdrop. Turned off, the invisible layer stays, so the page still does not scroll and a tap outside still closes. There is never one with modal={false}.

  • headerAligndefault "start"
    "start" | "center"

    Title and description alignment. center reads like an iOS sheet; the close button keeps both sides even.

  • dismissibledefault true
    boolean

    false: no drag to close, no outside tap, no Escape and no shortcut close, and no handle unless there are snap points. Only a close button or an action inside (DrawerClose, or setting open) closes it, e.g. terms that must be accepted.

  • modaldefault true
    boolean

    false: no overlay, no focus trap and no scroll lock; the page behind stays usable and readable, like a results sheet over a map. Focus is not moved into it on open.

  • handleOnlydefault false
    boolean

    Drags only from the handle, for content with its own horizontal gestures (a carousel, a map, a slider).

  • scaleBackgrounddefault false
    boolean

    Scales the page back behind the sheet like iOS. Needs vaul-drawer-wrapper="" (or data-vaul-drawer-wrapper="") on the element that wraps your app, e.g. <div vaul-drawer-wrapper="" className="min-h-dvh bg-background">. The demo marks its listings page.

  • repositionInputsdefault true
    boolean

    Moves the sheet up when the on-screen keyboard opens so the focused field stays visible. Turn it off for sheets that handle the keyboard themselves.

  • shortcutdefault ""
    string

    A key combo in mod+k notation (⌘ on Apple devices, Ctrl elsewhere) that toggles the drawer from anywhere on the page; the trigger gets aria-keyshortcuts. Empty binds nothing.

  • snapPoints
    (number | string)[]

    Stops on the way up: fractions of the screen ([0.45, 1]) or px strings (["320px", 1]). The sheet becomes full height, starts at the first point, the body only scrolls at the last one, and a tap on the handle steps to the next. Ignored in desktop dialog mode.

  • activeSnapPoint
    number | string | null

    The current snap point, controlled. Uncontrolled it starts at the first point and returns there after a close.

  • onActiveSnapPointChange
    (point: number | string | null) => void

    Called when a drag or a handle tap settles on another snap point.

  • fadeFromIndex
    number

    The snap point index from which the overlay dims. Defaults to the last point, so a half open sheet leaves the page bright.

  • container
    HTMLElement | null

    Portal target, document.body by default. Inside a transformed or contain:layout element the sheet, its snap points and its 92% height stay within that element (the map example).

  • onAnimationEnd
    (open: boolean) => void

    Runs after the open or close animation with the state it ended in, e.g. to reset a form once the sheet is gone.

  • classNames
    Partial<Record<"overlay" | "content" | "handle" | "close" | "header" | "title" | "description" | "body" | "footer", string>>

    Extra classes for the inner parts: the backdrop, the sheet, the handle, the close button, and the header, title, description, body and footer the simple API renders (the same keys as the sheet).

  • className
    string

    Extra classes for the sheet (drawer-content), merged last. Set [--drawer-radius:20px] here for a custom corner.

Parts

  • DrawerRoot

    component

    Compound API: holds the state and picks the mode (vaul sheet on phones; sheet, side panel or Radix dialog from 768px). No DOM. Rendered inside another drawer's content it nests on its own. The popover, dialog, dropdown and context menu sheets are built from these parts. Props: { open, defaultOpen, onOpenChange, direction, desktop, snapPoints, activeSnapPoint, onActiveSnapPointChange, fadeFromIndex, dismissible, modal, handleOnly, scaleBackground, repositionInputs, shortcut, container, onAnimationEnd }.

  • DrawerContent

    component

    The portal, the overlay (drawer-overlay), the sheet (drawer-content with data-mode, data-direction, data-variant, data-size), the handle and the close button. Needs a DrawerTitle inside (visuallyHidden if the design has none). Props: { size, height, variant, radius, showHandle, showClose, closeLabel, overlay, container, classNames } & Radix Dialog.Content props.

  • DrawerHeader

    component

    The title row and description. Its bottom hairline fades in once the body scrolls. Props: { title?, description?, hideTitle?, align?: "start" | "center" }.

  • DrawerTitle

    component

    17px semibold in sheets, 18px in panels and dialogs. visuallyHidden keeps it for screen readers only. Props: { visuallyHidden?: boolean } & Radix Dialog.Title props.

  • DrawerDescription

    component

    Muted 14px text, wired to aria-describedby. Props: Radix Dialog.Description props.

  • DrawerBody

    component

    The scroll region. Reports its scroll edges to the header and footer, pads the safe area when it is the last part, and stays still below the last snap point so the sheet drags instead. Takes div props.

  • DrawerFooter

    component

    Pinned actions, primary first. Stacked in sheets, a row in side and dialog modes; its top hairline shows while the body has more below. Takes div props.

  • DrawerTrigger

    component

    Opens the drawer from any element. Takes Radix Dialog.Trigger props (asChild).

  • DrawerClose

    component

    Closes the drawer from any element, also one that is not dismissible. Takes Radix Dialog.Close props (asChild).

  • useDrawer

    hook

    The nearest drawer's state, e.g. to close it after a form saves or to lay out differently in dialog mode. Signature: () => { open: boolean, setOpen: (open: boolean) => void, mode: "sheet" | "side" | "dialog", direction }.

Examples

Snap points over a map

snapPoints={[0.45, 1]} and modal={false}: it starts half open and the pins behind stay tappable. Drag it up to scroll the list.

Nested

A drawer opened from inside another one pushes the first one back, like iOS. The new address lands in the list.

Side panel on desktop

desktop="side": a floating cart panel from 768px, the same drawer as a sheet on a phone. Total in en-US.

Dialog on desktop

desktop="dialog": a centered dialog with a spring; useDrawer() sizes the buttons for the mode, onAnimationEnd resets the form.

Floating share sheet

variant="floating" keeps 8px off every edge with all corners round; the header is centered.

Not dismissible

dismissible={false}: no drag, handle, Escape or outside tap. Only accepting the terms closes it; read again later, it closes like any sheet.

From the top

direction="top": notifications drop in from above with the handle at the bottom edge.

Dependencies

@radix-ui/react-dialog@radix-ui/react-visually-hiddenlucide-reactmotionvaulbuttonmotionuse-controllable-stateuse-media-queryuse-scroll-edgesuse-shortcutui-config

Files

  • components/ui/drawer.tsx

FAQ

Is the React Drawer component free?

Yes. The source is free and needs no account: copy it from the Code tab or install it with the CLI.

How do I install Drawer?

Run npx wingo-ui@latest add drawer. It also adds the Wingo UI items it builds on (Button, Motion presets, useControllableState, useMediaQuery, useScrollEdges and 2 more) and the npm packages @radix-ui/react-dialog, @radix-ui/react-visually-hidden, lucide-react, motion and vaul. With shadcn, run npx shadcn@latest add https://wingo-ui.com/r/drawer.json.

See also

SheetDialogAlert DialogFamily DrawerPopoveruseScrollEdgeshook

Used in

Color PickerMobile App HomeLocation MapWorkflow CanvasCart drawerDate Range PickerUser MenuDropdown MenuFilter panelComboboxDate PickerDate Time Picker

and 32 more.

More React overlay components

Hover CardShare DialogShortcuts DialogTourTooltipContext Menu
All 13 React overlay components

Articles using this component

  • Shadcn Navbar: Mega Menu, Mobile Menu, Sticky Header

    Build it: screens and apps · Nov 5, 2026 · 9 min read

  • Chakra UI vs shadcn in 2026: Style Props or Tailwind?

    Comparisons and alternatives · Oct 16, 2026 · 10 min read

  • React Action Sheet: iOS-Style, Async and Responsive

    Mobile UI in React · Oct 16, 2026 · 9 min read

  • iOS Safari Keyboard Position Fixed: Keep a Composer Visible

    Mobile UI in React · Oct 13, 2026 · 9 min read

PreviousDialogNext Dropdown Menu

New York

Listings

$100 – $300NewElectronics
318 resultsNewest first

iPhone 13 mini, 128 GB, sealed

$290

Brooklyn · today, 2:32 PM

14″ Full HD laptop, new, with warranty

$280

Queens · yesterday

Tap Filters to open it. Swipe it away, tap outside or press Esc to close it.