A Sheet is a swipeable overlay that slides in from the edge of the viewport.
Position
The position prop determines which edge of the screen the sheet is anchored to. swipeDirection controls the direction it can be swiped to dismiss, which defaults to the same as position. Use start or end to build a navigation drawer, bottom for a mobile sheet, or top for a notification banner. start and end are automatically mirrored in right-to-left locales, while left and right are always physical.
import {SheetTrigger} from 'react-aria-components/Sheet';
import {Sheet, Heading} from './Sheet';
import {Button} from './Button';
import {Home, Compass, Library, Settings, LogOut} from 'lucide-react';
function Example() {
let items = [
{label: 'Home', icon: <Home size={20} />},
{label: 'Explore', icon: <Compass size={20} />},
{label: 'Library', icon: <Library size={20} />},
{label: 'Settings', icon: <Settings size={20} />}
];
return (
<SheetTrigger>
<Button variant="secondary">Open menu</Button>
<Sheet position="start">
<div style={{display: 'flex', flexDirection: 'column', height: '100%', alignItems: 'start'}}>
<Heading slot="title" style={{padding: '0 var(--spacing-3)', marginBottom: 16}}>Acme Music</Heading>
<nav style={{display: 'flex', flexDirection: 'column', gap: 4, flexGrow: 1, width: '100%'}}>
{items.map(item => (
<Button key={item.label} slot="close" variant="quiet" style={{justifyContent: 'start', gap: 12}}>
{item.icon}
<span>{item.label}</span>
</Button>
))}
</nav>
<Button slot="close" variant="secondary" style={{gap: 12, marginTop: 'auto'}}>
<LogOut size={20} />
<span>Sign out</span>
</Button>
</div>
</Sheet>
</SheetTrigger>
);
}
Snap points
By default, a sheet opens to reveal all of its content. Provide the snapPoints prop to make it stop partially open instead. Each value is the amount of the sheet that is visible, either as a number of pixels or a CSS length. Percentages are relative to the size of the sheet. The sheet opens at the first snap point, and can be dragged up to its full height or swiped down to dismiss.
import {SheetTrigger} from 'react-aria-components/Sheet';
import {Sheet, Heading} from './Sheet';
import {Button} from './Button';
import {Star, MapPin, Clock, Phone} from 'lucide-react';
import {Map} from './Map';
let reviews = [
{name: 'Maya R.', date: '2 days ago', text: 'The espresso was smooth and the staff was lovely. A great spot for a quiet morning.'},
{name: 'Jordan K.', date: '1 week ago', text: 'Really good pour-over and plenty of seating. It gets busy around lunch, but the line moves quickly.'},
{name: 'Avery L.', date: '2 weeks ago', text: 'The iced coffee was bright and refreshing, and the pastries were excellent. I will definitely be back.'},
{name: 'Chris T.', date: '1 month ago', text: 'Beautiful space with consistently good coffee. The cappuccino had the perfect amount of foam.'},
{name: 'Samira P.', date: '1 month ago', text: 'Friendly service, fast Wi-Fi, and a great neighborhood atmosphere. One of my favorite coffee shops in Port Marlow.'}
];
function Example() {
return (
<SheetTrigger>
<Button>Show place</Button>
<Sheet position="bottom" snapPoints={[280]}>
<Heading slot="title" style={{marginBottom: 4}}>Harborlight Coffee</Heading>
<div style={{display: 'flex', alignItems: 'center', gap: 8, color: 'var(--gray-1000)', marginBottom: 32}}>
<Star size={16} fill="currentColor" style={{color: '#ff9500'}} />
<strong>4.6</strong> · Coffee shop · $$
</div>
<div style={{display: 'flex', flexDirection: 'column', gap: 16, alignItems: 'start'}}>
<div style={{display: 'flex', gap: 12, alignItems: 'center'}}><MapPin size={18} /> 300 Harbor St, Port Marlow</div>
<div style={{display: 'flex', gap: 12, alignItems: 'center'}}><Clock size={18} /> Open until 6:00 PM</div>
<div style={{display: 'flex', gap: 12, alignItems: 'center'}}><Phone size={18} /> (510) 555-0142</div>
<p>Drag this sheet up to see more, or swipe down to dismiss. Snap points let you preview the most relevant content first, then expand for the full story.</p>
<Map />
<Button slot="close">Get directions</Button>
<h3 style={{margin: '8px 0 0'}}>Reviews</h3>
<div style={{display: 'flex', flexDirection: 'column', gap: 16, width: '100%'}}>
{reviews.map(review => (
<div key={review.name} style={{padding: 16, border: '1px solid var(--gray-300)', borderRadius: 12}}>
<div style={{display: 'flex', justifyContent: 'space-between', gap: 8, marginBottom: 8}}>
<strong>{review.name}</strong>
<span style={{color: 'var(--gray-700)', fontSize: 14}}>{review.date}</span>
</div>
<div style={{display: 'flex', gap: 2, color: '#ff9500', marginBottom: 8}}>
{Array.from({length: 5}, (_, i) => <Star key={i} size={14} fill="currentColor" />)}
</div>
<div>{review.text}</div>
</div>
))}
</div>
</div>
</Sheet>
</SheetTrigger>
);
}
Animation
Use the swipeAnimation prop on Sheet and SheetBackdrop to define a CSS keyframe animation that is synchronized with the swipe gesture and entry/exit animations. swipeAnimationRange specifies the snap points between which the animation plays.
import {SheetTrigger} from 'react-aria-components/Sheet';
import {Sheet, Heading} from './Sheet';
import {Button} from './Button';
function Example() {
return (
<SheetTrigger>
<Button>Settings</Button>
{/* By default, Sheet.tsx applies a fade transition to the backdrop.
This example adds a custom transform animation to the sheet. */}
<style>{`
@keyframes sheet-transform {
from {
transform-origin: top right;
transform: skew(10deg) scale(0.8);
}
to {
transform: skew(0deg) scale(1);
}
}`}</style>
<Sheet swipeAnimation="sheet-transform">
<Heading slot="title">Sheet</Heading>
<p style={{marginTop: 0}}>This sheet has a custom swipe animation.</p>
<div style={{height: 300, width: '100%', background: 'var(--gray-200)', borderRadius: 16, marginBottom: 16}} />
<Button slot="close">Close</Button>
</Sheet>
</SheetTrigger>
);
}
Stacking
Sheets can be nested to drill deeper into a flow. Set stackAnimation prop to a CSS keyframe animation to scale the parent sheet backward when a child is opened, synchronized with the swipe gesture. The Sheet component in this example wires this up automatically.
import {SheetTrigger} from 'react-aria-components/Sheet';
import {Sheet, Heading} from './Sheet';
import {Button} from './Button';
import {Form} from './Form';
import {TextField} from './TextField';
import {RadioGroup, Radio} from './RadioGroup';
import {CheckboxGroup} from './CheckboxGroup';
import {Checkbox} from './Checkbox';
import {ChevronRight} from 'lucide-react';
function Example() {
return (
<SheetTrigger>
<Button>Settings</Button>
<Sheet style={{height: 550}}>
<Heading slot="title">Settings</Heading>
<p style={{marginTop: 0, marginBottom: 32}}>Manage your account and preferences.</p>
<Form>
<TextField label="Homepage" defaultValue="react-aria.adobe.com" />
<RadioGroup label="Theme" defaultValue="auto">
<Radio value="auto">Auto</Radio>
<Radio value="light">Light</Radio>
<Radio value="dark">Dark</Radio>
</RadioGroup>
<SheetTrigger>
<Button variant="secondary">
<span>Account</span>
<ChevronRight size={18} />
</Button>
<Sheet style={{height: 550}}>
<Heading slot="title">Account</Heading>
<p style={{marginTop: 0, marginBottom: 32}}>Swipe this sheet away to return to Settings.</p>
<Form>
<TextField label="Name" defaultValue="Devon Govett" />
<TextField label="Username" defaultValue="@devongovett" />
<TextField label="Email" defaultValue="devon@example.com" />
<div style={{display: 'flex', justifyContent: 'space-between'}}>
<Button slot="close">Save</Button>
<SheetTrigger>
<Button variant="secondary">
<span>Advanced</span>
<ChevronRight size={18} />
</Button>
<Sheet style={{height: 550}}>
<Heading slot="title">Advanced</Heading>
<p style={{marginTop: 0, marginBottom: 32}}>Swipe this sheet away to return to Account.</p>
<Form>
<CheckboxGroup label="Notification settings" defaultValue={['account', 'dms']}>
<Checkbox value="account">Account activity</Checkbox>
<Checkbox value="mentions">Mentions</Checkbox>
<Checkbox value="dms">Direct message</Checkbox>
<Checkbox value="marketing">Marketing emails</Checkbox>
</CheckboxGroup>
<Button slot="close">Save</Button>
</Form>
</Sheet>
</SheetTrigger>
</div>
</Form>
</Sheet>
</SheetTrigger>
</Form>
</Sheet>
</SheetTrigger>
);
}
API
<SheetTrigger>
<Button />
<SheetOverlay>
<SheetBackdrop />
<Sheet>
<SheetContent>
<Heading slot="title" />
<Text slot="description" />
<Button slot="close" />
</SheetContent>
</Sheet>
</SheetOverlay>
</SheetTrigger>
SheetTrigger
A SheetTrigger opens a sheet when a trigger element is pressed.
| Name | Type | |
|---|---|---|
children | ReactNode | |
SheetOverlay
A SheetOverlay is a container for a SheetBackdrop and a Sheet.
| Name | Type | Default |
|---|---|---|
position | 'bottom'
| 'top'
| 'left'
| 'right'
| 'start'
| 'end'
| 'center' | Default: 'bottom'
|
The placement of the sheet on the screen. | ||
swipeDirection | 'bottom'
| 'top'
| 'vertical'
| 'left'
| 'right'
| 'start'
| 'end'
| 'horizontal' | Default: — |
The direction the sheet can be swiped. | ||
snapPoints | Array | Default: — |
Snap points the sheet will stop at, expressed as the amount of the sheet that is visible. Numbers are pixels, and percentages are relative to the size of the sheet. Sheets initially open to the first snap point. | ||
preventDismissal | boolean | Default: false
|
Whether to prevent the user from closing the sheet by swiping, pressing Escape, or interacting outside it. | ||
shouldCloseOnInteractOutside | | Default: — |
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. | ||
children | ChildrenOrFunction | Default: — |
The children of the component. A function may be provided to alter the children based on component state. | ||
Default className: react-aria-SheetOverlay
| Render Prop | CSS Selector |
|---|---|
position | CSS Selector: [data-position="bottom | top | left | right | center"]
|
The placement of the sheet on the screen. start and end are resolved to left or right
based on the locale direction. | |
swipeDirection | CSS Selector: [data-swipe-direction="bottom | top | vertical | left | right | horizontal"]
|
The direction the sheet can be swiped. start and end are resolved to left or right
based on the locale direction. | |
stackIndex | CSS Selector: [data-stack-index="0 | 1 | 2 | ..."]
|
| The index of the sheet in the stack. | |
hasDescendants | CSS Selector: [data-has-descendants]
|
| Whether the sheet has descendants. | |
isExpanded | CSS Selector: [data-expanded]
|
| Whether the sheet is fully expanded to its last snap point. | |
isEntering | CSS Selector: [data-entering]
|
| Whether the modal is currently entering. Use this to apply animations. | |
isExiting | CSS Selector: [data-exiting]
|
| Whether the modal is currently exiting. Use this to apply animations. | |
state | CSS Selector: — |
| State of the modal. | |
| CSS Variable |
|---|
--sheet-scroll-padding-y |
| The amount of the sheet obscured by browser UI or software keyboard along the vertical axis. Add as padding to the scrollable sheet content. |
--sheet-scroll-padding-x |
| The amount of the sheet obscured by browser UI along the horizontal axis. Add as padding to the scrollable sheet content. |
--visual-viewport-height |
| The height of the Visual Viewport, i.e. space above the software keyboard. |
--visual-viewport-width |
| The width of the Visual Viewport. |
SheetBackdrop
The backdrop behind a sheet.
| Name | Type | |
|---|---|---|
swipeAnimation | string | |
CSS | ||
swipeAnimationRange | | |
The snap point indices between which the swipe animation occurs. If omitted, the animation will occur over the full range of the sheet's movement. | ||
children | ChildrenOrFunction | |
The children of the component. A function may be provided to alter the children based on component state. | ||
Default className: react-aria-SheetBackdrop
| Render Prop | CSS Selector |
|---|---|
position | CSS Selector: [data-position="bottom | top | left | right | center"]
|
The placement of the sheet on the screen. start and end are resolved to left or right
based on the locale direction. | |
swipeDirection | CSS Selector: [data-swipe-direction="bottom | top | vertical | left | right | horizontal"]
|
The direction the sheet can be swiped. start and end are resolved to left or right
based on the locale direction. | |
stackIndex | CSS Selector: [data-stack-index="0 | 1 | 2 | ..."]
|
| The index of the sheet in the stack. | |
hasDescendants | CSS Selector: [data-has-descendants]
|
| Whether the sheet has descendants. | |
isExpanded | CSS Selector: [data-expanded]
|
| Whether the sheet is fully expanded to its last snap point. | |
isEntering | CSS Selector: [data-entering]
|
| Whether the modal is currently entering. Use this to apply animations. | |
isExiting | CSS Selector: [data-exiting]
|
| Whether the modal is currently exiting. Use this to apply animations. | |
state | CSS Selector: — |
| State of the modal. | |
Sheet
A Sheet is a swipeable overlay that slides in from the edge of the viewport.
| Name | Type | |
|---|---|---|
swipeAnimation | string | |
CSS | ||
swipeAnimationRange | | |
The snap point indices between which the swipe animation occurs. If omitted, the animation will occur over the full range of the sheet's movement. | ||
stackAnimation | string | |
CSS | ||
overscrollPadding | boolean | |
Whether to add padding to the sheet so that it appears to continue outside the viewport. | ||
children | ChildrenOrFunction | |
The children of the component. A function may be provided to alter the children based on component state. | ||
Default className: react-aria-Sheet
| Render Prop | CSS Selector |
|---|---|
position | CSS Selector: [data-position="bottom | top | left | right | center"]
|
The placement of the sheet on the screen. start and end are resolved to left or right
based on the locale direction. | |
swipeDirection | CSS Selector: [data-swipe-direction="bottom | top | vertical | left | right | horizontal"]
|
The direction the sheet can be swiped. start and end are resolved to left or right
based on the locale direction. | |
stackIndex | CSS Selector: [data-stack-index="0 | 1 | 2 | ..."]
|
| The index of the sheet in the stack. | |
hasDescendants | CSS Selector: [data-has-descendants]
|
| Whether the sheet has descendants. | |
isExpanded | CSS Selector: [data-expanded]
|
| Whether the sheet is fully expanded to its last snap point. | |
isEntering | CSS Selector: [data-entering]
|
| Whether the modal is currently entering. Use this to apply animations. | |
isExiting | CSS Selector: [data-exiting]
|
| Whether the modal is currently exiting. Use this to apply animations. | |
state | CSS Selector: — |
| State of the modal. | |
SheetContent
The scrollable content area of a sheet.
| Name | Type | |
|---|---|---|
children | ReactNode | | |
Children of the dialog. A function may be provided to access a function to close the dialog. | ||
Default className: react-aria-SheetContent