Drawer
A slide-in panel anchored to an edge of the viewport.
Examples
Section titled “Examples”Default
Section titled “Default”function DrawerContent({ onClose, title = 'Project details',}: { onClose: () => void; title?: string;}): React.JSX.Element { return ( <Layout content={ <LayoutContent> <Text as="p" color="secondary"> Review project activity, ownership, and open follow-ups. </Text> </LayoutContent> } footer={ <LayoutFooter primaryButton={ <Button label="Done" onClick={onClose} variant="primary" /> } secondaryButton={<Button label="Cancel" onClick={onClose} />} /> } header={<LayoutHeader title={title} />} /> );}
args => { const [isOpen, setIsOpen] = useState(false);
return ( <> <Button label="Open drawer" onClick={() => setIsOpen(true)} /> <Drawer {...args} isOpen={isOpen} onOpenChange={setIsOpen}> <DrawerContent onClose={() => setIsOpen(false)} /> </Drawer> </> );}With Layout
Section titled “With Layout”function DrawerContent({ onClose, title = 'Project details',}: { onClose: () => void; title?: string;}): React.JSX.Element { return ( <Layout content={ <LayoutContent> <Text as="p" color="secondary"> Review project activity, ownership, and open follow-ups. </Text> </LayoutContent> } footer={ <LayoutFooter primaryButton={ <Button label="Done" onClick={onClose} variant="primary" /> } secondaryButton={<Button label="Cancel" onClick={onClose} />} /> } header={<LayoutHeader title={title} />} /> );}
args => { // Must start closed: docs pages render this story as a live demo, and a // drawer that opens on mount covers the whole page via showModal(). const [isOpen, setIsOpen] = useState(false);
return ( <> <Button label="Open layout drawer" onClick={() => setIsOpen(true)} /> <Drawer {...args} isOpen={isOpen} onOpenChange={setIsOpen}> <DrawerContent onClose={() => setIsOpen(false)} /> </Drawer> </> );}Placement Variants
Section titled “Placement Variants”function DrawerContent({ onClose, title = 'Project details',}: { onClose: () => void; title?: string;}): React.JSX.Element { return ( <Layout content={ <LayoutContent> <Text as="p" color="secondary"> Review project activity, ownership, and open follow-ups. </Text> </LayoutContent> } footer={ <LayoutFooter primaryButton={ <Button label="Done" onClick={onClose} variant="primary" /> } secondaryButton={<Button label="Cancel" onClick={onClose} />} /> } header={<LayoutHeader title={title} />} /> );}
() => { // Placement is kept when the drawer closes so the exit animation slides // out of the same edge the drawer came from. const [placement, setPlacement] = useState<DrawerPlacement>('end'); const [isOpen, setIsOpen] = useState(false); const activeLabel = placements.find(option => option.placement === placement)?.label ?? 'Right';
return ( <> <div style={{display: 'flex', flexWrap: 'wrap', gap: 8}}> {placements.map(option => ( <Button key={option.placement} label={option.label} onClick={() => { setPlacement(option.placement); setIsOpen(true); }} /> ))} </div> <Drawer isOpen={isOpen} label={`${activeLabel} drawer`} onOpenChange={setIsOpen} placement={placement}> <DrawerContent onClose={() => setIsOpen(false)} title={`${activeLabel} drawer`} /> </Drawer> </> );}Custom Size
Section titled “Custom Size”function DrawerContent({ onClose, title = 'Project details',}: { onClose: () => void; title?: string;}): React.JSX.Element { return ( <Layout content={ <LayoutContent> <Text as="p" color="secondary"> Review project activity, ownership, and open follow-ups. </Text> </LayoutContent> } footer={ <LayoutFooter primaryButton={ <Button label="Done" onClick={onClose} variant="primary" /> } secondaryButton={<Button label="Cancel" onClick={onClose} />} /> } header={<LayoutHeader title={title} />} /> );}
() => { const [variant, setVariant] = useState<'numeric' | 'string' | null>(null);
return ( <> <div style={{display: 'flex', flexWrap: 'wrap', gap: 8}}> <Button label="Open 480px drawer" onClick={() => setVariant('numeric')} /> <Button label="Open 60vw drawer" onClick={() => setVariant('string')} /> </div> <Drawer isOpen={variant != null} label="Custom size drawer" onOpenChange={nextOpen => { if (!nextOpen) { setVariant(null); } }} placement="end" size={variant === 'string' ? '60vw' : 480}> <DrawerContent onClose={() => setVariant(null)} title={variant === 'string' ? '60vw drawer' : '480px drawer'} /> </Drawer> </> );}Unsaved Changes
Section titled “Unsaved Changes”args => { const [isOpen, setIsOpen] = useState(false);
return ( <> <Button label="Edit profile" onClick={() => setIsOpen(true)} /> <Drawer {...args} isOpen={isOpen} onOpenChange={setIsOpen}> <Layout content={ <LayoutContent> <div style={{display: 'flex', flexDirection: 'column', gap: 16}}> <Text as="p" color="secondary"> Backdrop and Escape dismissal are disabled, so unsaved edits can only be discarded through an explicit action. </Text> <TextInput label="Display name" onChange={() => {}} value="Ada Lovelace" /> </div> </LayoutContent> } footer={ <LayoutFooter primaryButton={ <Button label="Save" onClick={() => setIsOpen(false)} variant="primary" /> } secondaryButton={ <Button label="Discard" onClick={() => setIsOpen(false)} /> } /> } header={<LayoutHeader title="Edit profile" />} /> </Drawer> </> );}Imperative
Section titled “Imperative”() => { const drawer = useDrawer({label: 'Generated report', size: 420});
return ( <> <Button label="Show report" onClick={() => drawer.show( <Layout content={ <LayoutContent> <Text as="p" color="secondary"> The report is ready for review. </Text> </LayoutContent> } footer={ <LayoutFooter primaryButton={ <Button label="Close" onClick={drawer.hide} variant="primary" /> } /> } header={<LayoutHeader title="Generated report" />} />, ) } /> {drawer.element} </> );}Auto Focus
Section titled “Auto Focus”() => { const [isOpen, setIsOpen] = useState(false);
return ( <> <Button label="Open drawer" onClick={() => setIsOpen(true)} /> <Drawer isOpen={isOpen} label="Auto focus drawer" onOpenChange={setIsOpen} size={400}> <Layout content={ <LayoutContent> <div style={{ display: 'flex', flexDirection: 'column', gap: 12, }}> <button data-autofocus="true" style={{alignSelf: 'flex-start'}} type="button"> Focused action </button> <TextInput label="Notes" onChange={() => {}} placeholder="Add notes" value="" /> </div> </LayoutContent> } footer={ <LayoutFooter primaryButton={ <Button label="Close" onClick={() => setIsOpen(false)} variant="primary" /> } /> } header={<LayoutHeader title="Auto focus" />} /> </Drawer> </> );}Nested Content
Section titled “Nested Content”args => { const [isOpen, setIsOpen] = useState(false);
return ( <> <Button label="Edit workspace" onClick={() => setIsOpen(true)} /> <Drawer {...args} isOpen={isOpen} onOpenChange={setIsOpen}> <Layout content={ <LayoutContent label="Workspace settings"> <div style={{ display: 'flex', flexDirection: 'column', gap: 16, }}> <TextInput label="Workspace name" onChange={() => {}} value="Research Operations" /> <TextInput label="Owner" onChange={() => {}} value="Ada Lovelace" /> {Array.from({length: 10}, (_, index) => ( <Text as="p" color="secondary" key={index}> Setting group {index + 1}: Configure access, notifications, and workflow defaults for this workspace. </Text> ))} </div> </LayoutContent> } footer={ <LayoutFooter primaryButton={ <Button label="Save" onClick={() => setIsOpen(false)} variant="primary" /> } secondaryButton={ <Button label="Cancel" onClick={() => setIsOpen(false)} /> } /> } header={ <LayoutHeader subtitle="Manage workspace metadata and defaults." title="Edit workspace" /> } /> </Drawer> </> );}Drawer
Section titled “Drawer”A slide-in panel anchored to an edge of the viewport.
| Prop | Type | Default | Description |
|---|---|---|---|
children* | ReactNode | — | Drawer body content. |
className | string | — | Additional CSS class names applied to the drawer. |
data-testid | string | — | Test ID applied to the drawer. |
dismissBehavior | DrawerDismissBehavior | {isEscapeDismissEnabled: true, isBackdropDismissEnabled: true} | Controls whether Escape and backdrop clicks request dismissal. Pass a boolean to enable or disable both behaviors together. |
isOpen* | boolean | — | Whether the drawer is open. |
label* | string | — | Accessible label for the drawer. |
onOpenChange* | (isOpen: boolean) => void | — | Called when the drawer requests an open-state change. |
placement | "start" | "bottom" | "top" | "end" | 'end' | Edge of the viewport the drawer slides in from. |
ref | Ref<HTMLDialogElement> | — | Ref forwarded to the drawer element. |
size | SizeValue | — | Width (start/end) or height (top/bottom) of the drawer. Numbers are pixels, strings are used as-is. |
style | CSSProperties | — | Inline styles applied to the drawer. |