Bottom sheet
A modal sheet that slides up from the bottom of the screen.
import { BottomSheet } from '@aksornproject/aksara-ui'Basic sheet
BottomSheet is a namespace of parts. Use Trigger to open it and Close to close it. Pass a Button through render to style either control. The popup contains the scrim, handle and sheet.
<BottomSheet.Root>
<BottomSheet.Trigger render={<Button>Open filters</Button>} />
<BottomSheet.Popup>
<BottomSheet.Header title="Filters" subtitle="Choose a subject" />
<BottomSheet.Body>Choose the filters to apply.</BottomSheet.Body>
<BottomSheet.Footer>
<BottomSheet.Close render={<Button size="lg">Apply</Button>} />
</BottomSheet.Footer>
</BottomSheet.Popup>
</BottomSheet.Root>Heights
hug follows content up to 640/844 of the overlay height. capped and tall take 523/844 and 776/844 respectively. The body scrolls when needed.
<div className="flex gap-spacing-8">
{(['hug', 'capped', 'tall'] as const).map((height) => (
<BottomSheet.Root key={height}>
<BottomSheet.Trigger render={<Button>{height}</Button>} />
<BottomSheet.Popup height={height}>
<BottomSheet.Header title={height} />
<BottomSheet.Body>
Content scrolls when it exceeds the available height.
</BottomSheet.Body>
</BottomSheet.Popup>
</BottomSheet.Root>
))}
</div>Without a header or footer
Use aria-label or a BottomSheet.Title to name a sheet without a header. Set padding="flush" for edge-to-edge content.
<BottomSheet.Root>
<BottomSheet.Trigger render={<Button>Share</Button>} />
<BottomSheet.Popup aria-label="Share options">
<BottomSheet.Body padding="flush">
<BottomSheet.Close render={<Button>Copy link</Button>} />
</BottomSheet.Body>
</BottomSheet.Popup>
</BottomSheet.Root>Non-dismissible
A non-dismissible sheet ignores dragging, scrim presses and Escape. It must be controlled with open and onOpenChange. Close and the header close button still close it.
<BottomSheet.Root dismissible={false} open={open} onOpenChange={setOpen}>
<BottomSheet.Trigger render={<Button>Open required sheet</Button>} />
<BottomSheet.Popup>
<BottomSheet.Header title="Confirm" showClose={false} />
<BottomSheet.Body>Use the button to close this sheet.</BottomSheet.Body>
<BottomSheet.Footer>
<BottomSheet.Close render={<Button size="lg">Done</Button>} />
</BottomSheet.Footer>
</BottomSheet.Popup>
</BottomSheet.Root>Container
Pass container to render inside a positioned element. Heights follow that element rather than the viewport.
<div
ref={setFrame}
className="relative h-[500px] w-full max-w-[390px] overflow-hidden bg-color-background-body"
>
<BottomSheet.Root>
<BottomSheet.Trigger render={<Button>Open in frame</Button>} />
<BottomSheet.Popup container={frame}>
<BottomSheet.Header title="Contained sheet" />
<BottomSheet.Body>The sheet fills this frame.</BottomSheet.Body>
</BottomSheet.Popup>
</BottomSheet.Root>
</div>Popups inside a sheet
Select, Menu, Tooltip and nested Dialog popups portal into the sheet's host. They receive clicks and focus. Escape closes the inner popup without closing the sheet. A nested Dialog normally covers the viewport, even inside a contained sheet.
<BottomSheet.Root>
<BottomSheet.Trigger render={<Button>Open options</Button>} />
<BottomSheet.Popup>
<BottomSheet.Header title="Options" />
<BottomSheet.Body>
<Select.Root items={items}>
<Select.Trigger label="Subject" />
<Select.Portal>
<Select.Positioner>
<Select.Popup>
<Select.List>
{items.map((item) => (
<Select.Item key={item.value} value={item.value}>
{item.label}
</Select.Item>
))}
</Select.List>
</Select.Popup>
</Select.Positioner>
</Select.Portal>
</Select.Root>
<Menu.Root>
<Menu.Trigger>Actions</Menu.Trigger>
<Menu.Portal>
<Menu.Positioner>
<Menu.Popup>
<Menu.Item>Save</Menu.Item>
</Menu.Popup>
</Menu.Positioner>
</Menu.Portal>
</Menu.Root>
</BottomSheet.Body>
</BottomSheet.Popup>
</BottomSheet.Root>Anatomy
<BottomSheet.Root>
<BottomSheet.Trigger render={<Button>Open</Button>} />
<BottomSheet.Popup>
<BottomSheet.Header title="Filters" />
<BottomSheet.Body>Content</BottomSheet.Body>
<BottomSheet.Footer>
<BottomSheet.Close render={<Button>Done</Button>} />
</BottomSheet.Footer>
</BottomSheet.Popup>
</BottomSheet.Root>The footer stacks children by default and preserves their JSX order. Use className to change the layout.
Accessibility
- The header title names the sheet and its subtitle describes it. Without a header, provide a Title or an accessible label.
- Focus moves to the popup on open and returns to the trigger on close. Pass a ref or function to
initialFocusto focus another element. - The header close button is named "Close" by default. Pass
closeLabelto translate it. - The sheet is modal. Use a non-dismissible sheet only when the task needs an explicit action.
API
BottomSheet.Root
Prop
Type
BottomSheet.Trigger
Prop
Type
BottomSheet.Close
Prop
Type
BottomSheet.Popup
Prop
Type
BottomSheet.Header
Prop
Type
BottomSheet.Title
Prop
Type
BottomSheet.Description
Prop
Type
BottomSheet.Body
Prop
Type
BottomSheet.Footer
BottomSheetFooterProps accepts div props and a forwarded ref. Its className merges last.