Buttons
Data Display
Layout
Navigation
Drawer
A panel that slides in from any edge, with swipe to dismiss, snap points and an indented page behind it.
React
Examples
Basic
A bottom drawer with a grabber. Drag it down or press Escape to close it.
import { Portal } from "@ark-ui/react/portal"
import { field, input } from "@isbatak/panda-ds/recipes"
import { Button } from "@isbatak/react-ui/button"
import { Drawer } from "@isbatak/react-ui/drawer"
import { Switch } from "@isbatak/react-ui/switch"
export function DrawerDemo() {
const fieldClasses = field()
return (
<Drawer.Root placement="bottom">
<Drawer.Trigger asChild>
<Button>Edit settings</Button>
</Drawer.Trigger>
<Portal>
<Drawer.Backdrop />
<Drawer.Positioner>
<Drawer.Content>
<Drawer.Grabber>
<Drawer.GrabberIndicator />
</Drawer.Grabber>
<Drawer.Header>
<Drawer.Title>Update user settings</Drawer.Title>
</Drawer.Header>
<Drawer.Body display="flex" flexDirection="column" gap="4">
<Drawer.Description>Adjust your preferences, then drag the sheet down to close it.</Drawer.Description>
<div className={fieldClasses.root}>
<label className={fieldClasses.label} htmlFor="drawer-settings-username">
Username
</label>
<input id="drawer-settings-username" className={input()} placeholder="Enter your username" />
</div>
<div className={fieldClasses.root}>
<label className={fieldClasses.label} htmlFor="drawer-settings-email">
Email
</label>
<input
id="drawer-settings-email"
type="email"
className={input()}
placeholder="Enter your email address"
/>
</div>
<Switch.Root defaultChecked>
<Switch.Control>
<Switch.Thumb />
</Switch.Control>
<Switch.Label>Email notifications</Switch.Label>
<Switch.HiddenInput />
</Switch.Root>
</Drawer.Body>
<Drawer.Footer>
<Drawer.ActionTrigger asChild>
<Button variant="outline">Cancel</Button>
</Drawer.ActionTrigger>
<Button>Save</Button>
</Drawer.Footer>
</Drawer.Content>
</Drawer.Positioner>
</Portal>
</Drawer.Root>
)
}Swipe direction
Set placement to start, end, top or bottom to choose the edge the drawer slides in from. Drawer.Root turns
it into Zag's swipeDirection, so the drawer swipes back toward the same edge. start and end follow the text
direction: turn on right-to-left in the preview toolbar and they swap sides.
import { Portal } from "@ark-ui/react/portal"
import type { DrawerVariant } from "@isbatak/panda-ds/recipes"
import { Button } from "@isbatak/react-ui/button"
import { Drawer } from "@isbatak/react-ui/drawer"
import { useState } from "react"
type Placement = NonNullable<DrawerVariant["placement"]>
export function DrawerSwipeDirectionDemo() {
const [open, setOpen] = useState(false)
const [placement, setPlacement] = useState<Placement>("end")
const openAt = (nextPlacement: Placement) => {
setPlacement(nextPlacement)
setOpen(true)
}
return (
<>
<Button onClick={() => openAt("start")}>Start</Button>
<Button onClick={() => openAt("end")}>End</Button>
<Button onClick={() => openAt("top")}>Top</Button>
<Button onClick={() => openAt("bottom")}>Bottom</Button>
<Drawer.Root placement={placement} open={open} onOpenChange={(details) => setOpen(details.open)}>
<Portal>
<Drawer.Backdrop />
<Drawer.Positioner>
<Drawer.Content>
<Drawer.Header>
<Drawer.Title>Opens from {placement}</Drawer.Title>
</Drawer.Header>
<Drawer.Body>
<Drawer.Description>
Swipe it back toward the {placement} edge to close it. Turn on right-to-left in the preview toolbar
and start and end swap sides.
</Drawer.Description>
</Drawer.Body>
<Drawer.Footer>
<Drawer.ActionTrigger asChild>
<Button variant="outline">Close</Button>
</Drawer.ActionTrigger>
</Drawer.Footer>
</Drawer.Content>
</Drawer.Positioner>
</Portal>
</Drawer.Root>
</>
)
}Float
Set float to float the drawer inside the viewport, with a gap from the edge and rounded corners on every side. It
works with every swipe direction.
import { Portal } from "@ark-ui/react/portal"
import { Button } from "@isbatak/react-ui/button"
import { Drawer } from "@isbatak/react-ui/drawer"
export function DrawerFloatDemo() {
return (
<Drawer.Root placement="bottom" float>
<Drawer.Trigger asChild>
<Button>Float</Button>
</Drawer.Trigger>
<Portal>
<Drawer.Backdrop />
<Drawer.Positioner>
<Drawer.Content maxW="3xl" mx="auto">
<Drawer.Grabber>
<Drawer.GrabberIndicator />
</Drawer.Grabber>
<Drawer.Header flexDirection="column" alignItems="stretch" gap="1" pt="3">
<Drawer.Title>Float</Drawer.Title>
<Drawer.Description>
This drawer floats with a gap from the viewport edges and rounded corners.
</Drawer.Description>
</Drawer.Header>
<Drawer.Footer>
<Drawer.ActionTrigger asChild>
<Button variant="outline">Cancel</Button>
</Drawer.ActionTrigger>
<Button>Save</Button>
</Drawer.Footer>
</Drawer.Content>
</Drawer.Positioner>
</Portal>
</Drawer.Root>
)
}Snap points
snapPoints lists the heights the drawer can rest at, as fractions of the viewport or as lengths like "20rem".
defaultSnapPoint picks where it opens. A snap point never exceeds the content size, so give the content a height when
you snap to a fraction. The content gets data-expanded at its full height, which this example uses to let the list
scroll only then.
import { Portal } from "@ark-ui/react/portal"
import { Button } from "@isbatak/react-ui/button"
import { Drawer } from "@isbatak/react-ui/drawer"
import { styled } from "styled-system/jsx"
interface EventProps {
time: string
title: string
description: string
}
function Event({ time, title, description }: EventProps) {
return (
<styled.li display="flex" gap="4" py="4" borderBottomWidth="1px" _last={{ borderBottomWidth: "0" }}>
<styled.span w="12" flexShrink="0" color="fg.muted" fontVariantNumeric="tabular-nums">
{time}
</styled.span>
<div>
<styled.p fontWeight="medium">{title}</styled.p>
<styled.p color="fg.muted">{description}</styled.p>
</div>
</styled.li>
)
}
export function DrawerSnapPointsDemo() {
return (
<Drawer.Root placement="bottom" snapPoints={[0.5, 1]} defaultSnapPoint={0.5}>
<Drawer.Trigger asChild>
<Button>View itinerary</Button>
</Drawer.Trigger>
<Portal>
<Drawer.Backdrop />
<Drawer.Positioner>
<Drawer.Content h="80dvh">
<Drawer.Grabber>
<Drawer.GrabberIndicator />
</Drawer.Grabber>
<Drawer.Header>
<Drawer.Title>Team day itinerary</Drawer.Title>
</Drawer.Header>
<Drawer.Body css={{ overflowY: "hidden", "[data-expanded] &": { overflowY: "auto" } }}>
<Drawer.Description>
Swipe up to see the full schedule, or down to collapse or dismiss it.
</Drawer.Description>
<styled.ol mt="2">
<Event time="09:00" title="Coffee at the studio" description="Meet the team and plan the day." />
<Event time="10:00" title="Design workshop" description="Explore ideas for the next release." />
<Event time="12:00" title="Lunch together" description="Take a break at our favorite local spot." />
<Event time="14:00" title="Prototype review" description="Walk through the new interactions." />
<Event time="16:00" title="Team retrospective" description="Share what worked and what to improve." />
<Event time="17:00" title="Wrap up" description="Capture next steps before heading home." />
</styled.ol>
</Drawer.Body>
<Drawer.Footer>
<Drawer.ActionTrigger asChild>
<Button variant="outline">Done</Button>
</Drawer.ActionTrigger>
</Drawer.Footer>
</Drawer.Content>
</Drawer.Positioner>
</Portal>
</Drawer.Root>
)
}Modal
Set modal, trapFocus, preventScroll and closeOnInteractOutside to false to keep the page usable while the
drawer is open. Leave out Drawer.Backdrop so it doesn't cover the page.
import { Portal } from "@ark-ui/react/portal"
import { Button, CloseButton } from "@isbatak/react-ui/button"
import { Drawer } from "@isbatak/react-ui/drawer"
export function DrawerNonModalDemo() {
return (
<Drawer.Root modal={false} trapFocus={false} preventScroll={false} closeOnInteractOutside={false}>
<Drawer.Trigger asChild>
<Button>Open non-modal drawer</Button>
</Drawer.Trigger>
<Portal>
<Drawer.Positioner>
<Drawer.Content>
<Drawer.Header>
<Drawer.Title>Non-modal</Drawer.Title>
</Drawer.Header>
<Drawer.Body>
<Drawer.Description>
The page behind stays interactive. Scroll it or select text while this drawer is open.
</Drawer.Description>
</Drawer.Body>
<Drawer.CloseTrigger asChild>
<CloseButton size="sm" />
</Drawer.CloseTrigger>
</Drawer.Content>
</Drawer.Positioner>
</Portal>
</Drawer.Root>
)
}Controlled
Pass open and onOpenChange to open and close the drawer from outside and follow its state. Anything that sets the
state opens or closes it, so it needs no Drawer.Trigger.
import { Portal } from "@ark-ui/react/portal"
import { field, textarea } from "@isbatak/panda-ds/recipes"
import { Button, CloseButton } from "@isbatak/react-ui/button"
import { Drawer } from "@isbatak/react-ui/drawer"
import { useState } from "react"
export function DrawerControlledDemo() {
const [open, setOpen] = useState(false)
const fieldClasses = field()
return (
<>
<Button onClick={() => setOpen(true)}>Give feedback</Button>
<Drawer.Root open={open} onOpenChange={(details) => setOpen(details.open)}>
<Portal>
<Drawer.Backdrop />
<Drawer.Positioner>
<Drawer.Content>
<Drawer.Header>
<Drawer.Title>Send feedback</Drawer.Title>
</Drawer.Header>
<Drawer.Body display="flex" flexDirection="column" gap="4">
<Drawer.Description>Tell us what works and what we could do better.</Drawer.Description>
<div className={fieldClasses.root}>
<label className={fieldClasses.label} htmlFor="drawer-feedback">
Your feedback
</label>
<textarea
id="drawer-feedback"
className={textarea()}
rows={5}
placeholder="Type your feedback here"
/>
</div>
</Drawer.Body>
<Drawer.Footer>
<Button variant="outline" onClick={() => setOpen(false)}>
Cancel
</Button>
<Button onClick={() => setOpen(false)}>Send</Button>
</Drawer.Footer>
<Drawer.CloseTrigger asChild>
<CloseButton size="sm" />
</Drawer.CloseTrigger>
</Drawer.Content>
</Drawer.Positioner>
</Portal>
</Drawer.Root>
</>
)
}Scrollable
Long content scrolls inside Drawer.Body, so the header and footer stay in view. Swiping only starts once the body is
scrolled to its edge.
import { Portal } from "@ark-ui/react/portal"
import { Button } from "@isbatak/react-ui/button"
import { Drawer } from "@isbatak/react-ui/drawer"
import { styled } from "styled-system/jsx"
export function DrawerScrollableDemo() {
return (
<Drawer.Root size="sm">
<Drawer.Trigger asChild>
<Button>Read terms</Button>
</Drawer.Trigger>
<Portal>
<Drawer.Backdrop />
<Drawer.Positioner>
<Drawer.Content>
<Drawer.Header borderBottomWidth="1px">
<Drawer.Title>Terms of use</Drawer.Title>
</Drawer.Header>
<Drawer.Body display="flex" flexDirection="column" gap="3" py="4">
<Drawer.Description>The header and footer stay in place while this text scrolls.</Drawer.Description>
<styled.h3 fontWeight="semibold">Accounts</styled.h3>
<p>
When you create an account, you are responsible for keeping your password confidential and for all
activity that happens under your account.
</p>
<p>
Tell us right away if you notice any unauthorized use of your account or any other breach of security.
</p>
<styled.h3 fontWeight="semibold">Acceptable use</styled.h3>
<p>
Use the service only through the interfaces we provide. Do not attempt to bypass or break our security
measures, and do not scrape or mine data with automated tools.
</p>
<p>
Do not use the service to harass or harm other people. We may close accounts that break this rule
without notice.
</p>
<styled.h3 fontWeight="semibold">Your content</styled.h3>
<p>
You keep ownership of the content you submit. You give us a license to store, display and process it so
we can run the service for you.
</p>
<p>
You confirm that you have the rights to everything you upload and that it does not infringe on the
rights of anyone else.
</p>
<styled.h3 fontWeight="semibold">Termination</styled.h3>
<p>
We may suspend or end your access at any time if you break these terms. When that happens, your right to
use the service ends right away.
</p>
<p>
Sections that by their nature should survive termination, such as ownership and limitation of liability,
stay in effect.
</p>
<styled.h3 fontWeight="semibold">Liability</styled.h3>
<p>
The service is provided as is. We do not promise that it will always be available, error free or secure.
</p>
<p>
We are not liable for indirect or consequential damages, including lost profits or data, that come from
your use of the service.
</p>
<styled.h3 fontWeight="semibold">Changes</styled.h3>
<p>
We may update these terms. For material changes we give at least 30 days of notice before the new terms
take effect.
</p>
</Drawer.Body>
<Drawer.Footer borderTopWidth="1px" pt="4">
<Drawer.ActionTrigger asChild>
<Button variant="outline">Decline</Button>
</Drawer.ActionTrigger>
<Button>Accept</Button>
</Drawer.Footer>
</Drawer.Content>
</Drawer.Positioner>
</Portal>
</Drawer.Root>
)
}No drag area
Add data-no-drag to an element inside the content to stop drags from starting on it, here a row of swatches that
scrolls sideways.
import { Portal } from "@ark-ui/react/portal"
import { Button } from "@isbatak/react-ui/button"
import { Drawer } from "@isbatak/react-ui/drawer"
import { styled } from "styled-system/jsx"
const Swatch = styled("div", {
base: {
flexShrink: "0",
w: "28",
h: "20",
borderRadius: "l2",
},
})
export function DrawerNoDragAreaDemo() {
return (
<Drawer.Root placement="bottom">
<Drawer.Trigger asChild>
<Button>Pick a color</Button>
</Drawer.Trigger>
<Portal>
<Drawer.Backdrop />
<Drawer.Positioner>
<Drawer.Content>
<Drawer.Grabber>
<Drawer.GrabberIndicator />
</Drawer.Grabber>
<Drawer.Header>
<Drawer.Title>Pick a color</Drawer.Title>
</Drawer.Header>
<Drawer.Body display="flex" flexDirection="column" gap="4">
<Drawer.Description>
Scroll the swatches sideways. Drags that start on them scroll the row instead of moving the drawer.
</Drawer.Description>
<styled.div data-no-drag display="flex" gap="3" overflowX="auto" pb="2">
<Swatch bg="red.solid" />
<Swatch bg="orange.solid" />
<Swatch bg="yellow.solid" />
<Swatch bg="green.solid" />
<Swatch bg="teal.solid" />
<Swatch bg="blue.solid" />
<Swatch bg="purple.solid" />
<Swatch bg="pink.solid" />
</styled.div>
</Drawer.Body>
<Drawer.Footer>
<Drawer.ActionTrigger asChild>
<Button variant="outline">Done</Button>
</Drawer.ActionTrigger>
</Drawer.Footer>
</Drawer.Content>
</Drawer.Positioner>
</Portal>
</Drawer.Root>
)
}Non draggable
Pass draggable={false} to Drawer.Content so drags on the content don't move the drawer. The grabber stays a drag
handle; leave it out to turn off swipe to dismiss.
import { Portal } from "@ark-ui/react/portal"
import { Button } from "@isbatak/react-ui/button"
import { Drawer } from "@isbatak/react-ui/drawer"
export function DrawerNonDraggableDemo() {
return (
<Drawer.Root placement="bottom">
<Drawer.Trigger asChild>
<Button>Open drawer</Button>
</Drawer.Trigger>
<Portal>
<Drawer.Backdrop />
<Drawer.Positioner>
<Drawer.Content draggable={false}>
<Drawer.Grabber>
<Drawer.GrabberIndicator />
</Drawer.Grabber>
<Drawer.Header>
<Drawer.Title>Drag the handle</Drawer.Title>
</Drawer.Header>
<Drawer.Body>
<Drawer.Description>
Dragging the content does nothing. The grabber above still drags the drawer, so you can swipe it closed
from there.
</Drawer.Description>
</Drawer.Body>
<Drawer.Footer>
<Drawer.ActionTrigger asChild>
<Button variant="outline">Close</Button>
</Drawer.ActionTrigger>
</Drawer.Footer>
</Drawer.Content>
</Drawer.Positioner>
</Portal>
</Drawer.Root>
)
}Indent background
Wrap the page in Drawer.Stack and Drawer.Indent to shrink it back behind the drawer, over a
Drawer.IndentBackground. The indent follows the swipe, so the page grows again as you swipe the drawer away.
This box stands in for your page.
import { Portal } from "@ark-ui/react/portal"
import { Button } from "@isbatak/react-ui/button"
import { Drawer } from "@isbatak/react-ui/drawer"
import { styled } from "styled-system/jsx"
export function DrawerIndentBackgroundDemo() {
return (
<Drawer.Stack>
<styled.div position="relative" w="full" h="72" overflow="hidden" borderRadius="l3" borderWidth="1px">
<Drawer.IndentBackground />
<Drawer.Indent
minH="0"
h="full"
display="flex"
flexDirection="column"
alignItems="center"
justifyContent="center"
gap="4"
p="6"
>
<styled.p color="fg.muted" textAlign="center">
This box stands in for your page.
</styled.p>
<Drawer.Root placement="bottom">
<Drawer.Trigger asChild>
<Button>Open drawer</Button>
</Drawer.Trigger>
<Portal>
<Drawer.Backdrop />
<Drawer.Positioner>
<Drawer.Content>
<Drawer.Grabber>
<Drawer.GrabberIndicator />
</Drawer.Grabber>
<Drawer.Header>
<Drawer.Title>Indented page</Drawer.Title>
</Drawer.Header>
<Drawer.Body>
<Drawer.Description>
The page behind shrinks back while this drawer is open, and grows again as you swipe it away.
</Drawer.Description>
</Drawer.Body>
<Drawer.Footer>
<Drawer.ActionTrigger asChild>
<Button variant="outline">Close</Button>
</Drawer.ActionTrigger>
</Drawer.Footer>
</Drawer.Content>
</Drawer.Positioner>
</Portal>
</Drawer.Root>
</Drawer.Indent>
</styled.div>
</Drawer.Stack>
)
}Stacked
Open a drawer from inside another and the one below shrinks and peeks out behind it, through the --nested-drawers
variable. Closing the top drawer returns to the one behind it. Only the first drawer renders a backdrop.
import { Portal } from "@ark-ui/react/portal"
import { field, input } from "@isbatak/panda-ds/recipes"
import { Button } from "@isbatak/react-ui/button"
import { Drawer } from "@isbatak/react-ui/drawer"
import { SegmentGroup } from "@isbatak/react-ui/segment-group"
import { Switch } from "@isbatak/react-ui/switch"
import { Icon } from "../../ui/icon"
export function DrawerStackingDemo() {
const fieldClasses = field()
return (
<Drawer.Root placement="bottom" float>
<Drawer.Trigger asChild>
<Button>Workspace settings</Button>
</Drawer.Trigger>
<Portal>
<Drawer.Backdrop />
<Drawer.Positioner>
<Drawer.Content h="96" maxW="lg" mx="auto">
<Drawer.Grabber>
<Drawer.GrabberIndicator />
</Drawer.Grabber>
<Drawer.Header flexDirection="column" alignItems="stretch" gap="1" pt="3">
<Drawer.Title>Workspace settings</Drawer.Title>
<Drawer.Description>Set up your workspace and manage how your team collaborates.</Drawer.Description>
</Drawer.Header>
<Drawer.Body display="flex" flexDirection="column" alignItems="flex-start" gap="4">
<div className={fieldClasses.root}>
<label className={fieldClasses.label} htmlFor="drawer-workspace-name">
Workspace name
</label>
<input id="drawer-workspace-name" className={input()} defaultValue="Design studio" />
</div>
<Drawer.Root placement="bottom" float>
<Drawer.Trigger asChild>
<Button variant="subtle">
Team access
<Icon name="chevron-right" />
</Button>
</Drawer.Trigger>
<Portal>
<Drawer.Positioner>
<Drawer.Content h="96" maxW="lg" mx="auto">
<Drawer.Grabber>
<Drawer.GrabberIndicator />
</Drawer.Grabber>
<Drawer.Header flexDirection="column" alignItems="stretch" gap="1" pt="3">
<Drawer.Title>Team access</Drawer.Title>
<Drawer.Description>
Choose who can join. Close this sheet to return to your workspace.
</Drawer.Description>
</Drawer.Header>
<Drawer.Body display="flex" flexDirection="column" alignItems="flex-start" gap="4">
<SegmentGroup.Root defaultValue="invite" orientation="horizontal" aria-label="Who can join">
<SegmentGroup.Indicator />
<SegmentGroup.Item value="invite">
<SegmentGroup.ItemText>Invited people only</SegmentGroup.ItemText>
<SegmentGroup.ItemHiddenInput />
</SegmentGroup.Item>
<SegmentGroup.Item value="domain">
<SegmentGroup.ItemText>Company email</SegmentGroup.ItemText>
<SegmentGroup.ItemHiddenInput />
</SegmentGroup.Item>
</SegmentGroup.Root>
<Drawer.Root placement="bottom" float>
<Drawer.Trigger asChild>
<Button variant="subtle">
Member permissions
<Icon name="chevron-right" />
</Button>
</Drawer.Trigger>
<Portal>
<Drawer.Positioner>
<Drawer.Content h="96" maxW="lg" mx="auto">
<Drawer.Grabber>
<Drawer.GrabberIndicator />
</Drawer.Grabber>
<Drawer.Header flexDirection="column" alignItems="stretch" gap="1" pt="3">
<Drawer.Title>Member permissions</Drawer.Title>
<Drawer.Description>
Choose what members can do. Close this sheet to return to team access.
</Drawer.Description>
</Drawer.Header>
<Drawer.Body display="flex" flexDirection="column" gap="3">
<Switch.Root defaultChecked>
<Switch.Control>
<Switch.Thumb />
</Switch.Control>
<Switch.Label>Leave comments</Switch.Label>
<Switch.HiddenInput />
</Switch.Root>
<Switch.Root defaultChecked>
<Switch.Control>
<Switch.Thumb />
</Switch.Control>
<Switch.Label>Download files</Switch.Label>
<Switch.HiddenInput />
</Switch.Root>
<Switch.Root>
<Switch.Control>
<Switch.Thumb />
</Switch.Control>
<Switch.Label>Invite teammates</Switch.Label>
<Switch.HiddenInput />
</Switch.Root>
<Switch.Root>
<Switch.Control>
<Switch.Thumb />
</Switch.Control>
<Switch.Label>Create projects</Switch.Label>
<Switch.HiddenInput />
</Switch.Root>
</Drawer.Body>
<Drawer.Footer>
<Drawer.ActionTrigger asChild>
<Button>Back to team access</Button>
</Drawer.ActionTrigger>
</Drawer.Footer>
</Drawer.Content>
</Drawer.Positioner>
</Portal>
</Drawer.Root>
</Drawer.Body>
<Drawer.Footer>
<Drawer.ActionTrigger asChild>
<Button>Back to workspace</Button>
</Drawer.ActionTrigger>
</Drawer.Footer>
</Drawer.Content>
</Drawer.Positioner>
</Portal>
</Drawer.Root>
</Drawer.Body>
<Drawer.Footer>
<Drawer.ActionTrigger asChild>
<Button>Done</Button>
</Drawer.ActionTrigger>
</Drawer.Footer>
</Drawer.Content>
</Drawer.Positioner>
</Portal>
</Drawer.Root>
)
}Multiple triggers
Give each Drawer.Trigger a value to share one drawer. triggerValue on the drawer API says which one opened it, and
onTriggerValueChange reports it.
import { Portal } from "@ark-ui/react/portal"
import { Button } from "@isbatak/react-ui/button"
import { Drawer } from "@isbatak/react-ui/drawer"
export function DrawerMultipleTriggersDemo() {
return (
<Drawer.Root placement="end">
<Drawer.Trigger asChild value="inbox">
<Button>Inbox</Button>
</Drawer.Trigger>
<Drawer.Trigger asChild value="drafts">
<Button variant="outline">Drafts</Button>
</Drawer.Trigger>
<Drawer.Trigger asChild value="archive">
<Button variant="outline">Archive</Button>
</Drawer.Trigger>
<Portal>
<Drawer.Backdrop />
<Drawer.Positioner>
<Drawer.Content>
<Drawer.Context>
{(drawer) => (
<>
<Drawer.Header>
<Drawer.Title textTransform="capitalize">{drawer.triggerValue}</Drawer.Title>
</Drawer.Header>
<Drawer.Body>
<Drawer.Description>
The {drawer.triggerValue} button opened this drawer. Every trigger shares it, and triggerValue
says which one was used.
</Drawer.Description>
</Drawer.Body>
</>
)}
</Drawer.Context>
<Drawer.Footer>
<Drawer.ActionTrigger asChild>
<Button variant="outline">Close</Button>
</Drawer.ActionTrigger>
</Drawer.Footer>
</Drawer.Content>
</Drawer.Positioner>
</Portal>
</Drawer.Root>
)
}Using the API
Create the drawer with useDrawer and pass it to Drawer.RootProvider to drive it from anywhere: setOpen() and
setSnapPoint() change it, and open, snapPoint and triggerValue read it. useDrawer takes Zag's swipeDirection
directly, and the recipe styles the drawer from it.
import { Portal } from "@ark-ui/react/portal"
import { Button } from "@isbatak/react-ui/button"
import { Drawer, useDrawer } from "@isbatak/react-ui/drawer"
export function DrawerRootProviderDemo() {
const drawer = useDrawer({ swipeDirection: "down", snapPoints: [0.4, 1], defaultSnapPoint: 0.4 })
return (
<>
<Button onClick={() => drawer.setOpen(true)}>Open from outside</Button>
<Drawer.RootProvider value={drawer}>
<Portal>
<Drawer.Backdrop />
<Drawer.Positioner>
<Drawer.Content h="80dvh">
<Drawer.Grabber>
<Drawer.GrabberIndicator />
</Drawer.Grabber>
<Drawer.Header>
<Drawer.Title>Using the API</Drawer.Title>
</Drawer.Header>
<Drawer.Body>
<Drawer.Description>
The drawer is {drawer.snapPoint === 1 ? "expanded" : "at its first snap point"}.
</Drawer.Description>
</Drawer.Body>
<Drawer.Footer>
<Button variant="outline" onClick={() => drawer.setSnapPoint(drawer.snapPoint === 1 ? 0.4 : 1)}>
{drawer.snapPoint === 1 ? "Collapse" : "Expand"}
</Button>
<Button onClick={() => drawer.setOpen(false)}>Close</Button>
</Drawer.Footer>
</Drawer.Content>
</Drawer.Positioner>
</Portal>
</Drawer.RootProvider>
</>
)
}