Drawer
A drawer component for React.
Code
import { Badge, Button, Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger, Field, FieldContent, FieldDescription, FieldLabel, FieldTitle, RadioGroup, RadioGroupItem, toast,} from "@falcon/ui-kit";import { useState, useSyncExternalStore } from "react";
const deliveryTimes = [ { value: "asap", id: "delivery-asap", label: "Next available truck", description: "Today - dispatch assigns a driver now", badge: "Fastest", }, { value: "7-00", id: "delivery-7-00", label: "7:00 AM - 9:00 AM", description: "Loading starts at 6:30 AM", }, { value: "9-00", id: "delivery-9-00", label: "9:00 AM - 11:00 AM", description: "Good if you're spraying after lunch", }, { value: "1-00", id: "delivery-1-00", label: "1:00 PM - 3:00 PM", description: "Most popular - high demand", }, { value: "3-00", id: "delivery-3-00", label: "3:00 PM - 5:00 PM", description: "Last window before the warehouse closes", },];
export function Example() { const [open, setOpen] = useState(false); const [deliveryTime, setDeliveryTime] = useState("asap"); const isMobile = useIsMobile();
function handleConfirm() { const selected = deliveryTimes.find((time) => time.value === deliveryTime);
if (!selected) { return; }
setOpen(false); toast("Delivery time confirmed", { description: selected.label, }); }
return ( <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> We'll prepare your order as soon as possible. </DrawerDescription> </DrawerHeader> <div className="tw:flex-1 tw:overflow-y-auto tw:p-4"> <RadioGroup value={deliveryTime} onValueChange={setDeliveryTime} className="tw:gap-2" > {deliveryTimes.map((time) => ( <FieldLabel key={time.value} htmlFor={time.id}> <Field orientation="horizontal"> <FieldContent> <FieldTitle className="tw:flex tw:items-center tw:gap-2"> {time.label} {time.badge ? ( <Badge variant="secondary">{time.badge}</Badge> ) : null} </FieldTitle> <FieldDescription>{time.description}</FieldDescription> </FieldContent> <RadioGroupItem value={time.value} id={time.id} /> </Field> </FieldLabel> ))} </RadioGroup> </div> <DrawerFooter> <Button onClick={handleConfirm} className="tw:h-[34px]"> Confirm Delivery Time </Button> <DrawerClose render={<Button variant="outline" />}> Cancel </DrawerClose> </DrawerFooter> </DrawerContent> </Drawer> );}
const MOBILE_QUERY = "(max-width: 767px)";
function subscribeToIsMobile(onChange: () => void) { const mql = window.matchMedia(MOBILE_QUERY); mql.addEventListener("change", onChange); return () => mql.removeEventListener("change", onChange);}
function useIsMobile() { return useSyncExternalStore( subscribeToIsMobile, () => window.matchMedia(MOBILE_QUERY).matches, () => false, );}Composition
Section titled “Composition”Use the following composition to build a Drawer:
Drawer├── DrawerTrigger└── DrawerContent ├── DrawerHeader │ ├── DrawerTitle │ └── DrawerDescription └── DrawerFooterDrawerContent composes the portal, overlay, viewport, and popup. For
lower-level control, DrawerSwipeHandle is also exported.
Custom Sizes
Section titled “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 tw:h-* and tw:max-h-*
utilities on DrawerContent.
<DrawerContent className="tw:h-[50vh]">To customize the width of a side drawer, use the tw:w-* and tw:max-w-*
utilities on DrawerContent.
<DrawerContent className="tw:w-96">When the same component renders in multiple directions, scope an override to one
axis using the tw:data-[swipe-axis=*] variants.
<DrawerContent className="tw:data-[swipe-axis=y]:max-h-[50vh] tw:data-[swipe-axis=x]:w-96">To make a region of the drawer scrollable, make the scroll container a flex
item. Avoid tw:h-full, which does not resolve inside a content-sized drawer.
<DrawerContent> <DrawerHeader>...</DrawerHeader> <div className="tw:flex-1 tw:overflow-y-auto tw:p-4"> {/* Scrollable content */} </div> <DrawerFooter>...</DrawerFooter></DrawerContent>Styling
Section titled “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
tw:data-[swipe-direction=down]: on DrawerContent, or
tw: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. |
Position
Section titled “Position”Use the swipeDirection prop to set the side of the drawer.
Available options are up, right, down, and left.
Code
import { Button, Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger,} from "@falcon/ui-kit";
export function Example() { return ( <Drawer swipeDirection="left"> <DrawerTrigger render={<Button variant="secondary" />}> Open Left Drawer </DrawerTrigger> <DrawerContent> <DrawerHeader> <DrawerTitle>Order Summary</DrawerTitle> <DrawerDescription> Review line items before submitting. </DrawerDescription> </DrawerHeader> <div className="tw:flex-1 tw:p-4"> <div className="tw:size-full tw:rounded-2xl tw:bg-muted" /> </div> <DrawerFooter> <DrawerClose render={<Button />}>Close</DrawerClose> </DrawerFooter> </DrawerContent> </Drawer> );}Swipe Handle
Section titled “Swipe Handle”Use showSwipeHandle on Drawer to render a swipe handle.
Code
import { Button, Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger,} from "@falcon/ui-kit";
export function Example() { return ( <Drawer showSwipeHandle> <DrawerTrigger render={<Button variant="secondary" />}> Open Drawer </DrawerTrigger> <DrawerContent> <DrawerHeader> <DrawerTitle>Drawer</DrawerTitle> <DrawerDescription>Drawer with a swipe handle.</DrawerDescription> </DrawerHeader> <div className="tw:flex-1 tw:p-4"> <div className="tw:rounded-2xl tw:bg-muted tw:group-data-[swipe-axis=x]/drawer-popup:size-full tw:group-data-[swipe-axis=y]/drawer-popup:h-80 tw:group-data-[swipe-axis=y]/drawer-popup:w-full" /> </div> <DrawerFooter> <DrawerClose render={<Button />}>Close</DrawerClose> </DrawerFooter> </DrawerContent> </Drawer> );}Nested
Section titled “Nested”Open drawers from inside another drawer. Parent drawers stay mounted and stack behind the frontmost drawer.
Code
import { Button, Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger,} from "@falcon/ui-kit";import { useSyncExternalStore } from "react";
export function Example() { const isMobile = useIsMobile();
const swipeDirection = isMobile ? "down" : "right";
return ( <Drawer showSwipeHandle={isMobile} swipeDirection={swipeDirection}> <DrawerTrigger render={<Button variant="secondary" />}> Open Drawer </DrawerTrigger> <DrawerContent> <DrawerHeader> <DrawerTitle>Drawer</DrawerTitle> <DrawerDescription> Open another drawer from the same direction. </DrawerDescription> </DrawerHeader> <div className="tw:flex-1 tw:p-4"> <div className="tw:bg-muted tw:group-data-[swipe-axis=x]/drawer-popup:size-full tw:group-data-[swipe-axis=y]/drawer-popup:aspect-video tw:group-data-[swipe-axis=y]/drawer-popup:w-full" /> </div> <DrawerFooter> <Drawer showSwipeHandle={isMobile} swipeDirection={swipeDirection}> <DrawerTrigger render={<Button variant="outline" />}> Open Nested Drawer </DrawerTrigger> <DrawerContent> <DrawerHeader> <DrawerTitle>Nested Drawer</DrawerTitle> <DrawerDescription> The parent drawer stays mounted behind this one. </DrawerDescription> </DrawerHeader> <div className="tw:flex-1 tw:p-4"> <div className="tw:bg-muted tw:group-data-[swipe-axis=x]/drawer-popup:size-full tw:group-data-[swipe-axis=y]/drawer-popup:aspect-video tw:group-data-[swipe-axis=y]/drawer-popup:w-full" /> </div> <DrawerFooter> <Drawer showSwipeHandle={isMobile} swipeDirection={swipeDirection} > <DrawerTrigger render={<Button variant="outline" />}> Open Third Drawer </DrawerTrigger> <DrawerContent> <DrawerHeader> <DrawerTitle>Third Drawer</DrawerTitle> <DrawerDescription> Two drawers are stacked behind this one. </DrawerDescription> </DrawerHeader> <div className="tw:flex-1 tw:p-4"> <div className="tw:bg-muted tw:group-data-[swipe-axis=x]/drawer-popup:size-full tw:group-data-[swipe-axis=y]/drawer-popup:aspect-video tw:group-data-[swipe-axis=y]/drawer-popup:w-full" /> </div> <DrawerFooter> <Drawer showSwipeHandle={isMobile} swipeDirection={swipeDirection} > <DrawerTrigger render={<Button variant="outline" />}> Open Fourth Drawer </DrawerTrigger> <DrawerContent> <DrawerHeader> <DrawerTitle>Fourth Drawer</DrawerTitle> <DrawerDescription> This is the frontmost drawer in the stack. </DrawerDescription> </DrawerHeader> <div className="tw:flex-1 tw:p-4"> <div className="tw:bg-muted tw:group-data-[swipe-axis=x]/drawer-popup:size-full tw:group-data-[swipe-axis=y]/drawer-popup:aspect-video tw:group-data-[swipe-axis=y]/drawer-popup:w-full" /> </div> <DrawerFooter> <DrawerClose render={<Button variant="outline" />}> Close </DrawerClose> </DrawerFooter> </DrawerContent> </Drawer> <DrawerClose render={<Button variant="outline" />}> Close </DrawerClose> </DrawerFooter> </DrawerContent> </Drawer> <DrawerClose render={<Button variant="outline" />}> Close </DrawerClose> </DrawerFooter> </DrawerContent> </Drawer> <DrawerClose render={<Button variant="outline" />}>Close</DrawerClose> </DrawerFooter> </DrawerContent> </Drawer> );}
const MOBILE_QUERY = "(max-width: 767px)";
function subscribeToIsMobile(onChange: () => void) { const mql = window.matchMedia(MOBILE_QUERY); mql.addEventListener("change", onChange); return () => mql.removeEventListener("change", onChange);}
function useIsMobile() { return useSyncExternalStore( subscribeToIsMobile, () => window.matchMedia(MOBILE_QUERY).matches, () => false, );}Non Modal
Section titled “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.
Code
import { Button, Drawer, DrawerClose, DrawerContent, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger,} from "@falcon/ui-kit";
export function Example() { return ( <Drawer modal={false} disablePointerDismissal swipeDirection="right"> <DrawerTrigger render={<Button variant="outline" />}> Non Modal </DrawerTrigger> <DrawerContent> <DrawerHeader> <DrawerTitle>Non Modal Drawer</DrawerTitle> </DrawerHeader> <div className="tw:flex-1 tw:p-4"> <div className="tw:rounded-2xl tw:bg-muted tw:group-data-[swipe-axis=x]/drawer-popup:size-full tw:group-data-[swipe-axis=y]/drawer-popup:h-80 tw:group-data-[swipe-axis=y]/drawer-popup:w-full" /> </div> <DrawerFooter> <DrawerClose render={<Button />}>Close</DrawerClose> </DrawerFooter> </DrawerContent> </Drawer> );}Snap Points
Section titled “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 tw:data-expanded: variant.
Code
import { Button, Drawer, DrawerClose, DrawerContent, DrawerDescription, DrawerFooter, DrawerHeader, DrawerTitle, DrawerTrigger,} from "@falcon/ui-kit";
const SNAP_POINTS = ["10rem", 1];
export function Example() { return ( <Drawer snapPoints={SNAP_POINTS} showSwipeHandle> <DrawerTrigger render={<Button variant="outline" />}> Open Snap Drawer </DrawerTrigger> <DrawerContent> <DrawerHeader> <DrawerTitle>Snap points</DrawerTitle> <DrawerDescription> Drag the drawer to snap between a compact peek and a near full-height view. </DrawerDescription> </DrawerHeader> <div className="tw:flex-1 tw:p-4"> <div className="tw:rounded-2xl tw:bg-muted tw:group-data-[swipe-axis=x]/drawer-popup:size-full tw:group-data-[swipe-axis=y]/drawer-popup:h-80 tw:group-data-[swipe-axis=y]/drawer-popup:w-full" /> </div> <DrawerFooter> <DrawerClose render={<Button />}>Close</DrawerClose> </DrawerFooter> </DrawerContent> </Drawer> );}Responsive
Section titled “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.
Code
import { Button, Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle, DrawerTrigger, Input, Label,} from "@falcon/ui-kit";import { useCallback, useState, useSyncExternalStore } from "react";
export function Example() { const [open, setOpen] = useState(false); const isDesktop = useMediaQuery("(min-width: 768px)");
if (isDesktop) { return ( <Dialog open={open} onOpenChange={setOpen}> <DialogTrigger render={<Button variant="outline" />}> Edit Profile </DialogTrigger> <DialogContent className="tw:sm:max-w-[425px]"> <DialogHeader> <DialogTitle>Edit profile</DialogTitle> <DialogDescription> Make changes to your profile here. Click save when you're done. </DialogDescription> </DialogHeader> <ProfileForm /> </DialogContent> </Dialog> ); }
return ( <Drawer open={open} onOpenChange={setOpen}> <DrawerTrigger render={<Button variant="outline" />}> Edit Profile </DrawerTrigger> <DrawerContent> <DrawerHeader className="tw:text-left"> <DrawerTitle>Edit profile</DrawerTitle> <DrawerDescription> Make changes to your profile here. Click save when you're done. </DrawerDescription> </DrawerHeader> <ProfileForm className="tw:p-4" /> </DrawerContent> </Drawer> );}
function ProfileForm({ className }: React.ComponentProps<"form">) { return ( <form className={["tw:grid tw:items-start tw:gap-6", className] .filter(Boolean) .join(" ")} > <div className="tw:grid tw:gap-3"> <Label htmlFor="email">Email</Label> <Input type="email" id="email" defaultValue="mlarsen@example.com" /> </div> <div className="tw:grid tw:gap-3"> <Label htmlFor="username">Username</Label> <Input id="username" defaultValue="@mlarsen" /> </div> <Button type="submit">Save changes</Button> </form> );}
function useMediaQuery(query: string) { const subscribe = useCallback( (onChange: () => void) => { const mql = window.matchMedia(query); mql.addEventListener("change", onChange); return () => mql.removeEventListener("change", onChange); }, [query], );
return useSyncExternalStore( subscribe, () => window.matchMedia(query).matches, () => false, );}Drawer
Section titled “Drawer”Groups a drawer’s parts.
actionsRef
Section titled “actionsRef”actionsRef?: React.RefObject<DrawerRootActions | null>Ref to imperative close and unmount actions.
children
Section titled “children”children?: React.ReactNode | PayloadChildRenderFunction<unknown>The content of the drawer.
defaultOpen
Section titled “defaultOpen”defaultOpen?: booleanWhether the drawer is initially open. Use open to control it.
defaultSnapPoint
Section titled “defaultSnapPoint”defaultSnapPoint?: DrawerSnapPoint | nullInitial snap point for an uncontrolled drawer.
defaultTriggerId
Section titled “defaultTriggerId”defaultTriggerId?: string | nullID of the trigger associated with an initially open drawer.
disablePointerDismissal
Section titled “disablePointerDismissal”disablePointerDismissal?: booleanWhether to prevent the drawer from closing on outside presses.
handle
Section titled “handle”handle?: DrawerPrimitive.Handle<unknown>Associates the drawer with detached triggers.
modal?: boolean | "trap-focus" = trueDetermines if the drawer enters a modal state when open. true traps
focus, locks page scroll, and disables pointer interactions outside the
drawer. false allows interaction with the rest of the document.
'trap-focus' traps focus without locking scroll or disabling outside
pointer interactions.
onOpenChange
Section titled “onOpenChange”onOpenChange?: ((open: boolean, eventDetails: DrawerPrimitive.Root.ChangeEventDetails) => void)Called when the drawer opens or closes.
onOpenChangeComplete
Section titled “onOpenChangeComplete”onOpenChangeComplete?: ((open: boolean) => void)Called after opening or closing animations finish.
onSnapPointChange
Section titled “onSnapPointChange”onSnapPointChange?: ((snapPoint: DrawerSnapPoint | null, eventDetails: DrawerPrimitive.Root.SnapPointChangeEventDetails) => void)Called when the active snap point changes.
open?: booleanWhether the drawer is open.
showSwipeHandle
Section titled “showSwipeHandle”showSwipeHandle?: boolean = falseWhether DrawerContent renders a DrawerSwipeHandle.
snapPoint
Section titled “snapPoint”snapPoint?: DrawerSnapPoint | nullControlled active snap point.
snapPoints
Section titled “snapPoints”snapPoints?: DrawerSnapPoint[]Ordered positions where a vertical drawer settles.
snapToSequentialPoints
Section titled “snapToSequentialPoints”snapToSequentialPoints?: booleanWhether to disable velocity-based skipping between snap points.
swipeDirection
Section titled “swipeDirection”swipeDirection?: SwipeDirection = "down"Edge from which the drawer opens and toward which it dismisses.
triggerId
Section titled “triggerId”triggerId?: string | nullID of the trigger associated with a controlled drawer.
DrawerClose
Section titled “DrawerClose”Closes the associated drawer. Renders a <button> element and accepts
standard <button> props. Use render to replace the element.
className
Section titled “className”className?: string | ((state: DrawerCloseState) => string | undefined)CSS class applied to the element.
nativeButton
Section titled “nativeButton”nativeButton?: booleanWhether render returns a native button.
render
Section titled “render”render?: React.ReactElement<any, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DrawerCloseState>Replaces the rendered element or composes it with another component.
style?: React.CSSProperties | ((state: DrawerCloseState) => React.CSSProperties | undefined)Style applied to the element, or a function that returns one from state.
DrawerContent
Section titled “DrawerContent”Includes the portal, viewport, and popup, plus a backdrop when the drawer is
modal. Renders a <div> element and accepts standard <div> props. Use
render to replace the element. Use it with a DrawerTitle and
DrawerDescription so the drawer has an accessible name and description.
className
Section titled “className”className?: stringCSS class applied to the drawer.
finalFocus
Section titled “finalFocus”finalFocus?: boolean | React.RefObject<HTMLElement | null> | ((closeType: InteractionType) => boolean | HTMLElement | null | void)Determines the element to focus when the drawer closes.
initialFocus
Section titled “initialFocus”initialFocus?: boolean | React.RefObject<HTMLElement | null> | ((openType: InteractionType) => boolean | HTMLElement | null | void)Determines the element to focus when the drawer opens.
render
Section titled “render”render?: React.ReactElement<any, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DrawerPopupState>Replaces the rendered drawer or composes it with another component.
style?: React.CSSProperties | ((state: DrawerPopupState) => React.CSSProperties | undefined)Style applied to the drawer, or a function that returns one from state.
DrawerDescription
Section titled “DrawerDescription”Provides the drawer’s accessible description. Renders a <p> element and
accepts standard <p> props. Use render to replace the element.
className
Section titled “className”className?: stringCSS class applied to the element.
render
Section titled “render”render?: React.ReactElement<any, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DrawerDescriptionState>Replaces the rendered description or composes it with another component.
style?: React.CSSProperties | ((state: DrawerDescriptionState) => React.CSSProperties | undefined)Style applied to the description, or a function that returns one from state.
DrawerFooter
Section titled “DrawerFooter”Contains drawer actions. Renders a <div> element and accepts standard
<div> props.
DrawerHeader
Section titled “DrawerHeader”Groups DrawerTitle and DrawerDescription. Renders a <div> element and
accepts standard <div> props.
DrawerSwipeHandle
Section titled “DrawerSwipeHandle”Provides a pointer affordance for swiping the drawer. Renders a <div>
element and accepts standard <div> props. DrawerContent renders one
automatically when the root’s showSwipeHandle is set; render it directly
only for custom placement, leaving showSwipeHandle off to avoid a duplicate
handle.
DrawerTitle
Section titled “DrawerTitle”Provides the drawer’s accessible name. Renders an <h2> element and accepts
standard <h2> props. Use render to replace the element.
className
Section titled “className”className?: stringCSS class applied to the element.
render
Section titled “render”render?: React.ReactElement<any, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DrawerTitleState>Replaces the rendered title or composes it with another component.
style?: React.CSSProperties | ((state: DrawerTitleState) => React.CSSProperties | undefined)Style applied to the title, or a function that returns one from state.
DrawerTrigger
Section titled “DrawerTrigger”Opens the associated drawer. Renders a <button> element and accepts
standard <button> props. Use render to replace the element.
className
Section titled “className”className?: string | ((state: DrawerTriggerState) => string | undefined)CSS class applied to the element.
handle
Section titled “handle”handle?: DrawerPrimitive.Handle<unknown>Associates the trigger with a detached drawer.
id?: stringID of the trigger, used with triggerId in controlled mode.
nativeButton
Section titled “nativeButton”nativeButton?: booleanWhether render returns a native button.
payload
Section titled “payload”payload?: unknownPayload passed to a detached drawer.
render
Section titled “render”render?: React.ReactElement<any, string | React.JSXElementConstructor<any>> | ComponentRenderFn<HTMLProps, DrawerTriggerState>Replaces the rendered element or composes it with another component.
style?: React.CSSProperties | ((state: DrawerTriggerState) => React.CSSProperties | undefined)Style applied to the element, or a function that returns one from state.