All components
Drawer
Slide-in panels anchored to any viewport edge, with header, body, and footer slots.
123456789101112131415161718192021222324252627282930313233import { Drawer } from "#components/ui/ark-ui/drawer.tsx";import { Button } from "#components/ui/ark-ui/button.tsx";
// The drawer is Ark's Dialog machine wearing the drawer slot recipe —// placement slides the panel in from any viewport edge.export function Example() { return ( <Drawer.Root placement="end" size="xs"> <Drawer.Trigger asChild> <Button variant="surface">Open drawer</Button> </Drawer.Trigger> <Drawer.Portal> <Drawer.Backdrop /> <Drawer.Positioner> <Drawer.Content> <Drawer.Header> <Drawer.Title>Filters</Drawer.Title> <Drawer.CloseTrigger aria-label="Close">×</Drawer.CloseTrigger> </Drawer.Header> <Drawer.Body> <Drawer.Description> Narrow results by status, owner, or date range. </Drawer.Description> </Drawer.Body> <Drawer.Footer> <Button variant="solid">Apply</Button> </Drawer.Footer> </Drawer.Content> </Drawer.Positioner> </Drawer.Portal> </Drawer.Root> );}Variants
start
end
top
bottom
Sizes
xs
sm
md
lg
xl
Install
1npx stylexblocks add drawerSource
components/ui/ark-ui/drawer.tsx
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305import * as stylex from "@stylexjs/stylex";import { Dialog as ArkDialog } from "@ark-ui/react/dialog";import { Portal } from "@ark-ui/react/portal";import { createSlotRecipeContext } from "#lib/ark-ui/create-slot-recipe-context.tsx";import { defineSlotRecipe, fromMap, resolveSlotStyles,} from "#lib/recipe-props.ts";import { semanticRadii } from "@stylexblocks/ui/semantic-tokens/radii.stylex.ts";import { semanticShadows } from "@stylexblocks/ui/semantic-tokens/shadows.stylex.ts";import { durations } from '@stylexblocks/ui/tokens/durations.stylex.ts'import { fontSizes } from '@stylexblocks/ui/tokens/font-sizes.stylex.ts'import { fontWeights } from '@stylexblocks/ui/tokens/font-weights.stylex.ts'import { lineHeights } from '@stylexblocks/ui/tokens/line-heights.stylex.ts'import { semanticColors } from '@stylexblocks/ui/semantic-tokens/colors.stylex.ts'import { sizes } from '@stylexblocks/ui/tokens/sizes.stylex.ts'import { spacing } from '@stylexblocks/ui/tokens/spacing.stylex.ts'import { zIndices } from '@stylexblocks/ui/tokens/z-indices.stylex.ts'
const drawerBackdrop = stylex.create({ base: { position: 'fixed', insetInlineStart: 0, top: 0, width: '100vw', height: '100dvh', zIndex: zIndices.layer4, animationName: { default: null, ':is([data-open], [data-state="open"])': 'fade-in', ':is([data-closed], [data-state="closed"])': 'fade-out', }, animationDuration: { default: null, ':is([data-open], [data-state="open"])': durations.moderate, ':is([data-closed], [data-state="closed"])': durations.faster, }, },})
const drawerPositioner = stylex.create({ base: { '--drawer-z-index': zIndices.layer4, display: 'flex', width: '100vw', height: '100dvh', position: 'fixed', padding: spacing._2, insetInlineStart: 0, top: 0, zIndex: 'calc(var(--drawer-z-index) + var(--layer-index, 0))', },})
const drawerPositionerPlacements = stylex.create({ start: { justifyContent: 'flex-start', }, end: { justifyContent: 'flex-end', }, top: { alignItems: 'flex-start', }, bottom: { alignItems: 'flex-end', },})
const drawerPositionerAttacheds = stylex.create({ true: { padding: 0, },})
const drawerContent = stylex.create({ base: { display: 'flex', flexDirection: 'column', position: 'relative', width: '100%', outline: 'none', zIndex: 'calc(var(--drawer-z-index) + var(--layer-index, 0))', fontSize: fontSizes.sm, lineHeight: lineHeights.sm, maxHeight: '100dvh', color: 'inherit', animationDuration: { default: null, ':is([data-open], [data-state="open"])': durations.moderate, ':is([data-closed], [data-state="closed"])': durations.faster, }, },})
const drawerContentSizes = stylex.create({ xs: { maxWidth: sizes.xs, }, sm: { maxWidth: sizes.md, }, md: { maxWidth: sizes.lg, }, lg: { maxWidth: '2xl', }, xl: { maxWidth: '4xl', }, full: { maxWidth: '100vw', height: '100dvh', },})
const drawerContentPlacements = stylex.create({ start: { animationName: { default: null, ':is([data-open], [data-state="open"])': 'slide-from-left-full', ':is([data-closed], [data-state="closed"])': 'slide-to-left-full', }, }, end: { animationName: { default: null, ':is([data-open], [data-state="open"])': 'slide-from-right-full', ':is([data-closed], [data-state="closed"])': 'slide-to-right-full', }, }, top: { maxWidth: '100%', animationName: { default: null, ':is([data-open], [data-state="open"])': 'slide-from-top-full', ':is([data-closed], [data-state="closed"])': 'slide-to-top-full', }, }, bottom: { maxWidth: '100%', animationName: { default: null, ':is([data-open], [data-state="open"])': 'slide-from-bottom-full', ':is([data-closed], [data-state="closed"])': 'slide-to-bottom-full', }, },})
const drawerContentAttacheds = stylex.create({ true: { borderRadius: 'none', },})
const drawerHeader = stylex.create({ base: { display: 'flex', flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', flex: 0, paddingInline: spacing._6, paddingBlock: spacing._4, },})
const drawerBody = stylex.create({ base: { paddingInline: spacing._6, paddingBlock: spacing._2, flex: 1, overflow: 'auto', },})
const drawerFooter = stylex.create({ base: { display: 'flex', alignItems: 'center', justifyContent: 'flex-end', gap: spacing._3, paddingInline: spacing._6, paddingTop: spacing._2, paddingBottom: spacing._4, },})
const drawerTitle = stylex.create({ base: { fontSize: fontSizes.lg, lineHeight: lineHeights.lg, fontWeight: fontWeights.medium, },})
const drawerDescription = stylex.create({ base: { color: semanticColors.fgMuted, },})
const drawerCloseTrigger = stylex.create({ base: { position: 'absolute', top: spacing._4, right: spacing._4, },})
export type DrawerSize = keyof typeof drawerContentSizes
export type DrawerPlacement = keyof typeof drawerPositionerPlacements
export type DrawerAttached = keyof typeof drawerPositionerAttacheds
// defineSlotRecipe validates the shape at compile time: default values must// exist in the axis maps, and slot keys must be `styles` or a declared axis.// Axis maps are looked up by pluralized axis name (`placement` → `placements`).const drawerRecipe = defineSlotRecipe({ slots: { backdrop: { styles: drawerBackdrop }, positioner: { styles: drawerPositioner, placements: drawerPositionerPlacements, }, content: { styles: drawerContent, sizes: drawerContentSizes, placements: drawerContentPlacements, }, header: { styles: drawerHeader }, body: { styles: drawerBody }, footer: { styles: drawerFooter }, title: { styles: drawerTitle }, description: { styles: drawerDescription }, closeTrigger: { styles: drawerCloseTrigger }, }, defaultVariants: { size: "xs", placement: "end" },});
// Local chrome the recipe leaves to the composition layer: the backdrop's// fixed fill + wash and the panel's surface (plus real max-widths for the// `lg`/`xl` steps, whose generated values are Chakra size names).const chrome = stylex.create({ backdrop: { position: "fixed", inset: 0, backgroundColor: semanticColors.bgBackdrop, }, content: { backgroundColor: semanticColors.bgPanel, borderWidth: "1px", borderStyle: "solid", borderColor: semanticColors.border, borderRadius: semanticRadii.panel, boxShadow: semanticShadows.overlay, },});
const contentSizeFix = stylex.create({ lg: { maxWidth: "42rem" }, xl: { maxWidth: "56rem" },});
const { withRootProvider, withContext } = createSlotRecipeContext(drawerRecipe);
/** * Drawer — Ark's dialog machine wearing the drawer slot recipe: real portal * to `document.body`, focus trap, scroll lock, and dismiss behavior all come * from the machine, while `placement` slides the panel in from any viewport * edge. `<Drawer.Root size placement>` resolves the recipe once; every part * inherits the variants through context and composes via `asChild`. Portal * stays an unstyled engine part; Positioner is a recipe slot (placement). */export const Drawer = { Root: withRootProvider(ArkDialog.Root, "DrawerRoot"), Trigger: ArkDialog.Trigger, Context: ArkDialog.Context, Portal, Backdrop: withContext( ArkDialog.Backdrop, (v) => [...resolveSlotStyles(drawerRecipe.slots.backdrop, v), chrome.backdrop], "DrawerBackdrop", ), Positioner: withContext(ArkDialog.Positioner, "positioner", "DrawerPositioner"), Content: withContext( ArkDialog.Content, (v) => [ ...resolveSlotStyles(drawerRecipe.slots.content, v), chrome.content, fromMap(contentSizeFix, v.size), ], "DrawerContent", ), Header: withContext("div", "header", "DrawerHeader"), Body: withContext("div", "body", "DrawerBody"), Footer: withContext("div", "footer", "DrawerFooter"), Title: withContext(ArkDialog.Title, "title", "DrawerTitle"), Description: withContext(ArkDialog.Description, "description", "DrawerDescription"), CloseTrigger: withContext(ArkDialog.CloseTrigger, "closeTrigger", "DrawerCloseTrigger"),};