Drawer
An overlay panel that slides in from a screen edge, supporting sidebar navigation and bottom sheets.
Features
- Slides in from any screen edge (top, bottom, left, right) with spring animation
- Snap points with drag gestures and keyboard control
- Numeric and string snap points (
120,'50%','200px') resolved against drawer size - Automatic RTL flipping for left/right placement
- Focus trapping, backdrop dismiss, scroll lock via RAC Modal
prefers-reduced-motionsupport
Installation
npm install --save @godaddy/antaresProps
placementDrawerPlacement—Edge the drawer slides in from. left/right flip in RTL.
animate?boolean | undefinedtrueEnable/disable animation.
snapPoints?readonly DrawerSnapPoint[] | undefined—Snap point positions. Number = px, string = CSS length ('50%', '200px'). Percentages resolve against the drawer's rendered size. The largest resolved snap sets the drawer's minimum size along its constrained axis.
activeSnapPoint?DrawerSnapPoint | undefined—Current active snap point (controlled).
defaultActiveSnapPoint?DrawerSnapPoint | undefined—Initial active snap point (uncontrolled).
onSnapPointChange?((snapPoint: DrawerSnapPoint) => void) | undefined—Called when snap point changes.
snapLabels?string[] | undefined—Human-readable labels for snap points (for aria-valuetext).
maxSize?string | number | undefined'min(80vw, 400px)' for left/right, 'calc(100vh - 80px)' for top/bottomMax size of the drawer along its constrained axis. Accepts CSS values.
showCloseButton?boolean | undefinedtrue for top/bottom, false for left/rightShow built-in X close button.
closeLabel?string | undefined'Close'Accessible label for the close button.
snapSliderLabel?string | undefined'Resize drawer'Accessible label for the snap-point resize slider.
title?React.ReactNode—Accessible title rendered as a heading inside the drawer.
id?string | undefined—Forwarded to the inner Dialog for aria-controls linkage.
children?React.ReactNode—Content to render inside the drawer.
className?string | undefined—Additional CSS class for the inner dialog content region.
shouldCloseOnInteractOutside?((element: Element) => boolean) | undefined—When user interacts with the argument element outside of the overlay ref, return true if onClose should be called. This gives you a chance to filter out interaction with elements that should not dismiss the overlay. By default, onClose will always be called on interaction outside the overlay ref.
isEntering?boolean | undefined—Whether the modal is currently performing an entry animation.
isExiting?boolean | undefined—Whether the modal is currently performing an exit animation.
UNSTABLE_portalContainer?Element | undefineddocument.bodyThe container element in which the overlay portal will be placed. This may have unknown behavior depending on where it is portalled to.
isDismissable?boolean | undefinedfalseWhether to close the modal when the user interacts outside it.
isKeyboardDismissDisabled?boolean | undefinedfalseWhether pressing the escape key to close the modal should be disabled.
isOpen?boolean | undefined—Whether the overlay is open by default (controlled).
defaultOpen?boolean | undefined—Whether the overlay is open by default (uncontrolled).
onOpenChange?((isOpen: boolean) => void) | undefined—Handler that is called when the overlay's open state changes.
render?DOMRenderFunction<"div", ModalRenderProps> | undefined—Overrides the default DOM element with a custom render function. This allows rendering existing components with built-in styles and behaviors such as router links, animation libraries, and pre-styled components. Requirements: - You must render the expected element type (e.g. if `<button>` is expected, you cannot render an `<a>`). - Only a single root DOM element can be rendered (no fragments). - You must pass through props and ref to the underlying DOM element, merging with your own prop as appropriate.
slot?string | null | undefined—A slot name for the component. Slots allow the component to receive props from a parent component. An explicit `null` value indicates that the local props completely override all props received from a parent.
Sub-components
DrawerTrigger
Wraps a trigger button and a <Drawer>. Handles open state and aria-controls linkage automatically. Based on RAC DialogTrigger.
DrawerHandle
Visual drag handle for touch gestures. Renders a grab bar with a 44px touch target (24px for pointer:fine). When inside a <Drawer> with snapPoints, the handle becomes interactive and drag-to-snap activates automatically. Without snapPoints, the handle renders as a decorative affordance only.
| Prop | Type | Description |
|---|---|---|
className | string | Additional CSS class |
Examples
Basic Usage
Use <DrawerTrigger> to wrap a trigger button and a <Drawer>. The drawer opens when the trigger is pressed.
import { Drawer, DrawerTrigger, Button, Text } from '@godaddy/antares';export function DefaultExample() { return ( <DrawerTrigger> <Button variant="primary">Open drawer</Button> <Drawer placement="right" isDismissable title="Settings"> <Text>Drawer content goes here.</Text> </Drawer> </DrawerTrigger> );}Bottom Sheet
Use placement="bottom" with controlled state for a bottom sheet pattern.
import { useState } from 'react';import { Drawer, Button, Text } from '@godaddy/antares';export function BottomSheetExample() { const [open, setOpen] = useState(false); return ( <> <Button variant="primary" onPress={() => setOpen(true)}> Open bottom sheet </Button> <Drawer placement="bottom" isOpen={open} onOpenChange={setOpen} isDismissable title="Options"> <Text>Bottom sheet content with close button.</Text> </Drawer> </> );}Snap Points
Use snapPoints with <DrawerHandle /> for a resizable bottom sheet with touch gesture support.
import { useState } from 'react';import { Drawer, DrawerHandle, Button, Text } from '@godaddy/antares';export function SnapPointsExample() { const [open, setOpen] = useState(false); return ( <> <Button variant="primary" onPress={() => setOpen(true)}> Open snap sheet </Button> <Drawer placement="bottom" isOpen={open} onOpenChange={setOpen} isDismissable snapPoints={[148, 355]} defaultActiveSnapPoint={148} snapLabels={['Collapsed', 'Expanded']} title="Snap Sheet" > <DrawerHandle /> <Text>Drag the handle or use keyboard to resize.</Text> </Drawer> </> );}Percentage Snap Points
Use string snap points like '30%' and '60%' for sizes relative to the drawer's rendered dimensions.
import { useState } from 'react';import { Drawer, DrawerHandle, Button, Text } from '@godaddy/antares';export function PercentSnapPointsExample() { const [open, setOpen] = useState(false); return ( <> <Button variant="primary" onPress={() => setOpen(true)}> Open percent snap </Button> <Drawer placement="bottom" isOpen={open} onOpenChange={setOpen} isDismissable snapPoints={['30%', '60%']} defaultActiveSnapPoint="30%" snapLabels={['Collapsed', 'Expanded']} title="Percent Snap" > <DrawerHandle /> <Text>Snap points use percentage of drawer size.</Text> </Drawer> </> );}Controlled Snap Points
Use activeSnapPoint and onSnapPointChange for full control over the active snap point.
import { useState } from 'react';import { Drawer, DrawerHandle, Button, Flex, Text, type DrawerSnapPoint } from '@godaddy/antares';const SNAP_POINTS = [148, 355];export function ControlledSnapExample() { const [open, setOpen] = useState(false); const [snap, setSnap] = useState<DrawerSnapPoint>(355); return ( <> <Button variant="primary" onPress={() => setOpen(true)}> Open controlled snap </Button> <Text>Active snap: {snap}px</Text> <Drawer placement="bottom" isOpen={open} onOpenChange={setOpen} isDismissable snapPoints={SNAP_POINTS} activeSnapPoint={snap} onSnapPointChange={setSnap} snapLabels={['Collapsed', 'Expanded']} title="Controlled Snap" > <DrawerHandle /> <Flex gap="md"> <Button variant="primary" onPress={() => setSnap(148)}> Collapse </Button> <Button variant="primary" onPress={() => setSnap(355)}> Expand </Button> </Flex> </Drawer> </> );}Nested Popover
Popovers rendered inside a drawer stay open without dismissing the drawer.
import { useState } from 'react';import { Drawer, Button, Text, Popover, PopoverTrigger } from '@godaddy/antares';export function NestedPopoverExample() { const [open, setOpen] = useState(false); return ( <> <Button variant="primary" onPress={() => setOpen(true)}> Open drawer </Button> <Drawer placement="right" isOpen={open} onOpenChange={setOpen} isDismissable title="Drawer with Popover"> <PopoverTrigger> <Button variant="primary">Open popover</Button> <Popover> <Text>Popover inside drawer</Text> </Popover> </PopoverTrigger> </Drawer> </> );}Accessibility
Keyboard
| Key | Action |
|---|---|
Escape | Close the drawer (when isDismissable or not isKeyboardDismissDisabled) |
Tab | Move focus within the drawer (focus is trapped) |
Arrow Up / Right | Move to next snap point (when snap slider is focused) |
Arrow Down / Left | Move to previous snap point |
Home | Jump to first snap point |
End | Jump to last snap point |
In RTL, horizontal arrow key direction is reversed for the snap slider.
ARIA
role="dialog"witharia-modal="true"on the drawer panel (via RAC Modal)- Dialog accessible name via
title(renders a heading) oraria-label(for titleless drawers) aria-label="Close"on the built-in close buttonrole="slider"on the snap point keyboard control witharia-valuenow,aria-valuemin,aria-valuemax,aria-valuetextaria-hidden="true"on the visual drag handle (decorative)
Best Practices
- Always provide a
titleoraria-labelprop so the dialog has an accessible name. - Use
isDismissableto allow closing via backdrop click. - Don't nest drawers.
Checkbox
Accessible checkbox component for multi-select options with support for indeterminate states, validation, keyboard navigation, and flexible group layouts.
DropZone
A standalone region that accepts drag-and-drop file interactions, rendering a clearly defined target area where users can drop files.