{
  "title": "Sheet / Drawer",
  "description": "Slide-from-edge modal panel. Bottom sheet by default; side drawer with side=\"left\" or side=\"right\". Drawer is an alias for Sheet.",
  "url": "/docs/components/sheet",
  "since": "0.5.0",
  "tags": [
    "overlay"
  ],
  "platform": "both",
  "source": "---\ntitle: Sheet / Drawer\ndescription: Slide-from-edge modal panel. Bottom sheet by default; side drawer with side=\"left\" or side=\"right\". Drawer is an alias for Sheet.\nsince: 0.5.0\ntags: [overlay]\nplatform: both\ncategory: overlays\n---\n\nimport { BundleSize } from '@/components/bundle-size';\nimport { Preview } from '@/components/preview';\nimport { PropsTable } from '@/components/props-table';\n\n<BundleSize component=\"Sheet\" />\n\n## At a glance\n\n- Slide-from-edge modal panel. Defaults to a bottom sheet; set `side=\"right\"` / `\"left\"` / `\"top\"` for drawers.\n- Compound parts: `Sheet.Trigger`, `Sheet.Panel`, `Sheet.Header`, `Sheet.Title`, `Sheet.Description`, `Sheet.Body`, `Sheet.Footer`, `Sheet.Close`. Triggers and closes use `asChild` by default.\n- `Drawer` is an alias — `import { Drawer } from '@nori-ui/core'` is identical to `Sheet` at runtime.\n- Cross-platform: RN `<Modal>` on native (slide animation), CSS transitions on web with focus-trap, body scroll-lock, Escape-to-close, and click-outside-to-close.\n- Safe-area aware: leaves space for device home indicators on native via bottom padding in `Sheet.Footer`.\n- Accessibility: `role=\"dialog\"`, `aria-modal=\"true\"`, `aria-labelledby` wired to `Sheet.Title`, `aria-describedby` wired to `Sheet.Description`.\n\n## Preview\n\n<Preview name=\"sheet-basic\" />\n\n## Bottom sheet\n\nThe default. Add `side=\"bottom\"` explicitly or rely on the default:\n\n```tsx\nimport { useState } from 'react';\nimport { Button, Sheet } from '@nori-ui/core';\n\nexport function BottomSheetExample() {\n    return (\n        <Sheet side=\"bottom\" size=\"md\">\n            <Sheet.Trigger>\n                <Button>Open sheet</Button>\n            </Sheet.Trigger>\n            <Sheet.Panel>\n                <Sheet.Header>\n                    <Sheet.Title>Settings</Sheet.Title>\n                    <Sheet.Description>Manage your preferences.</Sheet.Description>\n                </Sheet.Header>\n                <Sheet.Body>\n                    {/* arbitrary content */}\n                </Sheet.Body>\n                <Sheet.Footer>\n                    <Sheet.Close>\n                        <Button variant=\"secondary\">Cancel</Button>\n                    </Sheet.Close>\n                    <Sheet.Close>\n                        <Button>Done</Button>\n                    </Sheet.Close>\n                </Sheet.Footer>\n            </Sheet.Panel>\n        </Sheet>\n    );\n}\n```\n\n## Side panel\n\n<Preview name=\"sheet-side-panel\" />\n\n```tsx\nimport { Button, Sheet } from '@nori-ui/core';\n\nexport function SidePanelExample() {\n    return (\n        <Sheet side=\"right\" size=\"md\">\n            <Sheet.Trigger>\n                <Button>Open side panel</Button>\n            </Sheet.Trigger>\n            <Sheet.Panel>\n                <Sheet.Header>\n                    <Sheet.Title>Settings</Sheet.Title>\n                    <Sheet.Description>Manage your account preferences.</Sheet.Description>\n                </Sheet.Header>\n                <Sheet.Body>\n                    {/* navigation items, settings list, etc. */}\n                </Sheet.Body>\n                <Sheet.Footer>\n                    <Sheet.Close>\n                        <Button>Save</Button>\n                    </Sheet.Close>\n                </Sheet.Footer>\n            </Sheet.Panel>\n        </Sheet>\n    );\n}\n```\n\n## Sizes\n\n`size` accepts a preset key or an explicit pixel number:\n\n| Value | Panel dimension |\n|---|---|\n| `'sm'` | 25% of viewport height (or width for side drawers) |\n| `'md'` | 50% — default |\n| `'lg'` | 75% |\n| `'full'` | 100% |\n| `number` | Explicit pixel value, e.g. `size={400}` |\n\n```tsx\n<Sheet side=\"bottom\" size=\"lg\">…</Sheet>\n<Sheet side=\"right\" size={320}>…</Sheet>\n```\n\n## Controlled\n\n```tsx\nimport { useState } from 'react';\nimport { Button, Sheet } from '@nori-ui/core';\n\nexport function ControlledSheet() {\n    const [open, setOpen] = useState(false);\n\n    async function handleSave() {\n        await saveSettings();\n        setOpen(false);\n    }\n\n    return (\n        <Sheet open={open} onOpenChange={setOpen} side=\"bottom\" size=\"md\">\n            <Sheet.Trigger>\n                <Button>Edit</Button>\n            </Sheet.Trigger>\n            <Sheet.Panel>\n                <Sheet.Header>\n                    <Sheet.Title>Edit profile</Sheet.Title>\n                </Sheet.Header>\n                <Sheet.Body>\n                    {/* form fields */}\n                </Sheet.Body>\n                <Sheet.Footer>\n                    <Button onPress={handleSave}>Save</Button>\n                </Sheet.Footer>\n            </Sheet.Panel>\n        </Sheet>\n    );\n}\n```\n\n## Non-dismissible\n\nSet `dismissible={false}` to require the user to use an explicit close button:\n\n```tsx\n<Sheet dismissible={false} side=\"bottom\">\n    <Sheet.Trigger>\n        <Button>Open</Button>\n    </Sheet.Trigger>\n    <Sheet.Panel>\n        <Sheet.Header>\n            <Sheet.Title>Required action</Sheet.Title>\n        </Sheet.Header>\n        <Sheet.Body>Please complete this form before continuing.</Sheet.Body>\n        <Sheet.Footer>\n            <Sheet.Close>\n                <Button>Done</Button>\n            </Sheet.Close>\n        </Sheet.Footer>\n    </Sheet.Panel>\n</Sheet>\n```\n\n## Drawer alias\n\n`Drawer` and `Sheet` are the same component at runtime:\n\n```tsx\nimport { Drawer } from '@nori-ui/core';\n\nexport function DrawerExample() {\n    return (\n        <Drawer side=\"right\" size=\"md\">\n            <Drawer.Trigger>\n                <Button>Open drawer</Button>\n            </Drawer.Trigger>\n            <Drawer.Panel>\n                <Drawer.Header>\n                    <Drawer.Title>Navigation</Drawer.Title>\n                </Drawer.Header>\n                <Drawer.Body>\n                    {/* nav items */}\n                </Drawer.Body>\n            </Drawer.Panel>\n        </Drawer>\n    );\n}\n```\n\n## Scroll behavior\n\nWrap `Sheet.Body` content in a `ScrollView` (native) or `div` with `overflow: auto` (web) when the content may exceed the panel height:\n\n```tsx\nimport { ScrollView } from 'react-native';\nimport { Sheet } from '@nori-ui/core';\n\n<Sheet.Body>\n    <ScrollView>\n        {longList.map((item) => (\n            <Item key={item.id} {...item} />\n        ))}\n    </ScrollView>\n</Sheet.Body>\n```\n\n## Accessibility notes\n\n- The panel receives `role=\"dialog\"` and `aria-modal=\"true\"`.\n- `Sheet.Title` is wired to `aria-labelledby` on the panel. Always include it.\n- `Sheet.Description` is wired to `aria-describedby`. Include when context is needed.\n- On web: focus moves into the panel on open, is trapped there, and returns to the trigger on close. Escape dismisses (unless `dismissible={false}`).\n- On native: the RN Modal manages focus isolation automatically.\n\n## Open state — `open`, `defaultOpen`, `onOpenChange`\n\n`open` (controlled) + `onOpenChange` is the parent-driven shape. Pass `defaultOpen` for uncontrolled mode. Mixing the two prefers controlled. Trigger and Close flip the state automatically, so most call sites need neither prop.\n\n## API reference\n\n### Sheet\n\n<PropsTable component=\"Sheet\" />\n\n### Sheet.Trigger\n\n<PropsTable component=\"SheetTrigger\" />\n\n### Sheet.Panel\n\n<PropsTable component=\"SheetPanel\" />\n\n### Sheet.Header\n\n<PropsTable component=\"SheetHeader\" />\n\n### Sheet.Title\n\n<PropsTable component=\"SheetTitle\" />\n\n### Sheet.Description\n\n<PropsTable component=\"SheetDescription\" />\n\n### Sheet.Body\n\n<PropsTable component=\"SheetBody\" />\n\n### Sheet.Footer\n\n<PropsTable component=\"SheetFooter\" />\n\n### Sheet.Close\n\n<PropsTable component=\"SheetClose\" />\n"
}
