Installation
npx @park-ui/cli add drawerAdd Component
Copy the code snippet below into you components folder.
'use client'
import { Dialog } from '@ark-ui/react/dialog'
import { ark } from '@ark-ui/react/factory'
import type { ComponentProps } from 'react'
import { createStyleContext } from 'styled-system/jsx'
import { drawer } from 'styled-system/recipes'
const { withRootProvider, withContext } = createStyleContext(drawer)
export type RootProps = ComponentProps<typeof Root>
export const Root = withRootProvider(Dialog.Root, {
defaultProps: { unmountOnExit: true, lazyMount: true },
})
export const RootProvider = withRootProvider(Dialog.Root, {
defaultProps: { unmountOnExit: true, lazyMount: true },
})
export const Backdrop = withContext(Dialog.Backdrop, 'backdrop')
export const Positioner = withContext(Dialog.Positioner, 'positioner')
export const CloseTrigger = withContext(Dialog.CloseTrigger, 'closeTrigger')
export const Content = withContext(Dialog.Content, 'content')
export const Description = withContext(Dialog.Description, 'description')
export const Title = withContext(Dialog.Title, 'title')
export const Trigger = withContext(Dialog.Trigger, 'trigger')
export const Body = withContext(ark.div, 'body')
export const Header = withContext(ark.div, 'header')
export const Footer = withContext(ark.div, 'footer')
export { DialogContext as Context } from '@ark-ui/react/dialog'
Integrate Recipe
Integrate this recipe in to your Panda config.
import { dialogAnatomy } from '@ark-ui/react/dialog'
import { defineSlotRecipe } from '@pandacss/dev'
export const drawer = defineSlotRecipe({
className: 'drawer',
slots: dialogAnatomy.extendWith('header', 'body', 'footer').keys(),
base: {
backdrop: {
background: 'black.a7',
position: 'fixed',
insetInlineStart: '0',
top: '0',
width: '100vw',
height: '100dvh',
zIndex: 'overlay',
_open: {
animationName: 'fade-in',
animationTimingFunction: 'emphasized-in',
animationDuration: 'slow',
},
_closed: {
animationName: 'fade-out',
animationTimingFunction: 'emphasized-out',
animationDuration: 'normal',
},
},
positioner: {
display: 'flex',
width: '100vw',
height: '100dvh',
position: 'fixed',
insetInlineStart: '0',
top: '0',
zIndex: 'modal',
overscrollBehaviorY: 'none',
},
content: {
display: 'flex',
flexDirection: 'column',
position: 'relative',
width: '100%',
outline: 0,
zIndex: 'modal',
maxH: '100dvh',
color: 'inherit',
bg: 'gray.surface.bg',
boxShadow: 'lg',
_open: {
animationDuration: 'slowest',
animationTimingFunction: 'cubic-bezier(0.05, 0.7, 0.1, 1.0)',
},
_closed: {
animationDuration: 'normal',
animationTimingFunction: 'cubic-bezier(0.3, 0.0, 0.8, 0.15)',
},
},
header: {
display: 'flex',
flexDirection: 'column',
gap: '1',
pt: { base: '4', md: '6' },
pb: '4',
px: { base: '4', md: '6' },
flex: '0',
},
body: {
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
flex: '1',
overflow: 'auto',
p: { base: '4', md: '6' },
},
footer: {
display: 'flex',
alignItems: 'center',
justifyContent: 'flex-end',
flex: '0',
gap: '3',
py: '4',
px: { base: '4', md: '6' },
},
title: {
color: 'fg.default',
fontWeight: 'semibold',
textStyle: 'xl',
},
description: {
color: 'fg.muted',
textStyle: 'sm',
},
closeTrigger: {
pos: 'absolute',
top: '3',
insetEnd: '3',
},
},
defaultVariants: {
placement: 'end',
size: 'sm',
},
variants: {
size: {
xs: {
content: {
maxW: 'xs',
},
},
sm: {
content: {
maxW: 'sm',
},
},
md: {
content: {
maxW: 'md',
},
},
lg: {
content: {
maxW: 'lg',
},
},
xl: {
content: {
maxW: 'xl',
},
},
full: {
content: {
maxW: '100vw',
h: '100dvh',
},
},
},
placement: {
start: {
positioner: {
justifyContent: 'flex-start',
alignItems: 'stretch',
},
content: {
_open: {
animationName: {
base: 'slide-from-left-full, fade-in',
_rtl: 'slide-from-right-full, fade-in',
},
},
_closed: {
animationName: {
base: 'slide-to-left-full, fade-out',
_rtl: 'slide-to-right-full, fade-out',
},
},
},
},
end: {
positioner: {
justifyContent: 'flex-end',
alignItems: 'stretch',
},
content: {
_open: {
animationName: {
base: 'slide-from-right-full, fade-in',
_rtl: 'slide-from-left-full, fade-in',
},
},
_closed: {
animationName: {
base: 'slide-to-right-full, fade-out',
_rtl: 'slide-to-left-full, fade-out',
},
},
},
},
top: {
positioner: {
justifyContent: 'stretch',
alignItems: 'flex-start',
},
content: {
maxW: '100%',
_open: { animationName: 'slide-from-top-full, fade-in' },
_closed: { animationName: 'slide-to-top-full, fade-out' },
},
},
bottom: {
positioner: {
justifyContent: 'stretch',
alignItems: 'flex-end',
},
content: {
maxW: '100%',
_open: { animationName: 'slide-from-bottom-full, fade-in' },
_closed: { animationName: 'slide-to-bottom-full, fade-out' },
},
},
},
},
})
Usage
import { Drawer } from '@/components/ui'
<Drawer.Root>
<Drawer.Backdrop />
<Drawer.Trigger />
<Drawer.Positioner>
<Drawer.Content>
<Drawer.CloseTrigger />
<Drawer.Header>
<Drawer.Title />
</Drawer.Header>
<Drawer.Body />
<Drawer.Footer />
</Drawer.Content>
</Drawer.Positioner>
</Drawer.Root>
Examples
Sizes
Use the size prop to change the size of the drawer.
Placements
Use the placement prop to change the placement of the drawer.
Props
| Prop | Default | Type |
|---|---|---|
size | 'sm' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'full' |
placement | 'end' | 'start' | 'end' | 'top' | 'bottom' |
closeOnEscape | true | booleanWhether to close the dialog when the escape key is pressed |
closeOnInteractOutside | true | booleanWhether to close the dialog when the outside is clicked |
defaultOpen | false | booleanThe initial open state of the dialog when rendered. Use when you don't need to control the open state of the dialog. |
lazyMount | false | booleanWhether to enable lazy mounting |
modal | true | booleanWhether to prevent pointer interaction outside the element and hide all content below it |
preventScroll | true | booleanWhether to prevent scrolling behind the dialog when it's opened |
role | \dialog\ | 'dialog' | 'alertdialog'The dialog's role |
skipAnimationOnMount | false | booleanWhether to allow the initial presence animation. |
trapFocus | true | booleanWhether to trap focus inside the dialog when it's opened |
unmountOnExit | false | booleanWhether to unmount on exit. |
aria-label | stringHuman readable label for the dialog, in event the dialog title is not rendered | |
finalFocusEl | () => MaybeElementElement to receive focus when the dialog is closed | |
id | stringThe unique identifier of the machine. | |
ids | Partial<{
trigger: string
positioner: string
backdrop: string
content: string
closeTrigger: string
title: string
description: string
}>The ids of the elements in the dialog. Useful for composition. | |
immediate | booleanWhether to synchronize the present change immediately or defer it to the next frame | |
initialFocusEl | () => MaybeElementElement to receive focus when the dialog is opened | |
onEscapeKeyDown | (event: KeyboardEvent) => voidFunction called when the escape key is pressed | |
onExitComplete | VoidFunctionFunction called when the animation ends in the closed state | |
onFocusOutside | (event: FocusOutsideEvent) => voidFunction called when the focus is moved outside the component | |
onInteractOutside | (event: InteractOutsideEvent) => voidFunction called when an interaction happens outside the component | |
onOpenChange | (details: OpenChangeDetails) => voidFunction to call when the dialog's open state changes | |
onPointerDownOutside | (event: PointerDownOutsideEvent) => voidFunction called when the pointer is pressed down outside the component | |
onRequestDismiss | (event: LayerDismissEvent) => voidFunction called when this layer is closed due to a parent layer being closed | |
open | booleanThe controlled open state of the dialog | |
persistentElements | (() => Element | null)[]Returns the persistent elements that: - should not have pointer-events disabled - should not trigger the dismiss event | |
present | booleanWhether the node is present (controlled by the user) | |
restoreFocus | booleanWhether to restore focus to the element that had focus before the dialog was opened |