Sheet

A Sheet is a swipeable overlay that slides in from the edge of the viewport.

Theme 
position 
swipeDirection 
preventDismissal 
overscrollPadding 
Example
Sheet.tsx
Sheet.css
import {useState} from 'react';
import {SheetTrigger} from 'react-aria-components/Sheet';
import {Sheet, Heading} from './Sheet';
import {Button} from './Button';
import {Minus, Plus, ShoppingCart} from 'lucide-react';

function Example(props) {
  let [items, setItems] = useState([
    {name: 'Pork Bao', detail: 'Steamed · 2 pieces', emoji: '🥟', price: 4.5, qty: 2},
    {name: 'Spicy Ramen', detail: 'Extra spicy · Soft egg', emoji: '🍜', price: 13, qty: 1},
    {name: 'Mango Boba', detail: 'Less ice · 50% sweet', emoji: '🧋', price: 5.75, qty: 1}
  ]);
  let update = (name, delta) => setItems(items => items
    .map(item => item.name === name ? {...item, qty: item.qty + delta} : item)
    .filter(item => item.qty > 0));
  let subtotal = items.reduce((sum, item) => sum + item.price * item.qty, 0);
  let delivery = items.length ? 2.99 : 0;

  return (
    <SheetTrigger>
      <Button>
        <ShoppingCart size={18} />
        <span>Cart ({items.reduce((n, item) => n + item.qty, 0)})</span>
      </Button>
      <Sheet {...props}>
</Sheet> </SheetTrigger> ); }

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.

NameType
childrenReactNode

SheetOverlay

A SheetOverlay is a container for a SheetBackdrop and a Sheet.

NameTypeDefault
position'bottom' | 'top' | 'left' | 'right' | 'start' | 'end' | 'center'Default: 'bottom'

The placement of the sheet on the screen. start and end are mirrored in right-to-left locales.

swipeDirection'bottom' | 'top' | 'vertical' | 'left' | 'right' | 'start' | 'end' | 'horizontal'Default: —

The direction the sheet can be swiped. start and end are mirrored in right-to-left locales. Defaults to the same direction as position.

snapPointsArray<number | string>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.

preventDismissalbooleanDefault: false

Whether to prevent the user from closing the sheet by swiping, pressing Escape, or interacting outside it.

shouldCloseOnInteractOutside(element: Element) => booleanDefault: —

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<>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 PropCSS Selector
positionCSS 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.
swipeDirectionCSS 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.
stackIndexCSS Selector: [data-stack-index="0 | 1 | 2 | ..."]
The index of the sheet in the stack.
hasDescendantsCSS Selector: [data-has-descendants]
Whether the sheet has descendants.
isExpandedCSS Selector: [data-expanded]
Whether the sheet is fully expanded to its last snap point.
isEnteringCSS Selector: [data-entering]
Whether the modal is currently entering. Use this to apply animations.
isExitingCSS Selector: [data-exiting]
Whether the modal is currently exiting. Use this to apply animations.
stateCSS 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.

NameType
swipeAnimationstring

CSS @keyframes name for the animation that occurs while the user swipes.

swipeAnimationRange{
start?: number,
end?: number
}

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<>

The children of the component. A function may be provided to alter the children based on component state.

Default className: react-aria-SheetBackdrop

Render PropCSS Selector
positionCSS 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.
swipeDirectionCSS 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.
stackIndexCSS Selector: [data-stack-index="0 | 1 | 2 | ..."]
The index of the sheet in the stack.
hasDescendantsCSS Selector: [data-has-descendants]
Whether the sheet has descendants.
isExpandedCSS Selector: [data-expanded]
Whether the sheet is fully expanded to its last snap point.
isEnteringCSS Selector: [data-entering]
Whether the modal is currently entering. Use this to apply animations.
isExitingCSS Selector: [data-exiting]
Whether the modal is currently exiting. Use this to apply animations.
stateCSS Selector: —
State of the modal.

Sheet

A Sheet is a swipeable overlay that slides in from the edge of the viewport.

NameType
swipeAnimationstring

CSS @keyframes name for the animation that occurs while the user swipes.

swipeAnimationRange{
start?: number,
end?: number
}

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.

stackAnimationstring

CSS @keyframes name for the animation that occurs when a sheet has descendants.

overscrollPaddingboolean

Whether to add padding to the sheet so that it appears to continue outside the viewport.

children<>

The children of the component. A function may be provided to alter the children based on component state.

Default className: react-aria-Sheet

Render PropCSS Selector
positionCSS 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.
swipeDirectionCSS 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.
stackIndexCSS Selector: [data-stack-index="0 | 1 | 2 | ..."]
The index of the sheet in the stack.
hasDescendantsCSS Selector: [data-has-descendants]
Whether the sheet has descendants.
isExpandedCSS Selector: [data-expanded]
Whether the sheet is fully expanded to its last snap point.
isEnteringCSS Selector: [data-entering]
Whether the modal is currently entering. Use this to apply animations.
isExitingCSS Selector: [data-exiting]
Whether the modal is currently exiting. Use this to apply animations.
stateCSS Selector: —
State of the modal.

SheetContent

The scrollable content area of a sheet.

NameType
childrenReactNode | (opts: ) => ReactNode

Children of the dialog. A function may be provided to access a function to close the dialog.

Default className: react-aria-SheetContent