Sheet
Sheet overlays content on top of a full-page view and often block the UI below. Sheet includes a Modal variant, which should be used to present content, tasks, or actions that require exclusive user focus.
Design guidance
When and how to use this
A Sheet should be preferred over the other overlay components for mobile-first layouts, especially those surfaced within native applications.
When to consider something else
If the content to be displayed in the overlay needs to take advantage of additional screen space on tablet and desktop screen sizes, consider using a Modal with responsive layout for the content.
React
Render the Sheet component itself unconditionally using the isOpen and
onClose props to control its open state. The
useDisclosure is purpose-built to manage
the open state of overlays.
() => {
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
<Sheet
footer={
<VStack alignItems="stretch" spacing="300">
<Button size="large">Sign up or Log in</Button>
<Button size="large">Maybe later</Button>
</VStack>
}
header="Save your favorites"
isClosable
isOpen={isOpen}
onClose={onClose}
>
<Body>Sign up or log in to add this photo to your favorites.</Body>
</Sheet>
<Button iconBefore={iconCellPhone} onClick={onOpen}>
Open Sheet
</Button>
</>
);
};Scrollable Content
When displaying long-form content in a Sheet, you likely want to restrict
the height of the scrollable container on mobile when the Sheet is anchored
to the bottom of the screen. Pass maxContentHeight to control the maximum
height of the scroll container.
() => {
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
<Sheet
footer={
<VStack alignItems="stretch" spacing="300">
<Button size="large" onClick={onClose}>
Accept
</Button>
<Button size="large" onClick={onClose}>
Cancel
</Button>
</VStack>
}
header="Terms & Privacy"
isClosable
isOpen={isOpen}
onClose={onClose}
size="full"
>
<Heading size={300}>Terms of Use</Heading>
<LoremIpsum generate={{ p: 2 }} />
<Heading size={300}>Privacy Policy</Heading>
<LoremIpsum generate={{ p: 3, sentences: 5 }} />
</Sheet>
<Button iconBefore={iconUser} onClick={onOpen}>
Terms & Privacy
</Button>
</>
);
};| Prop | Description |
|---|---|
header | Sets the heading content. The value can be a simple string, in which case it is rendered as the appropriate Heading style. A JSX fragment can also be provided. |
footer | Sets the footer content. When provided, footer content is wrapped in an HStack by default with the appropriate spacing. If the footer content is wrapped in a layout container (Box, Stack, etc.), it will be rendered as the footer element. |
isClosable | Whether to display the close button. Defaults to true. |
size | Size of the Sheet component; which can be auto, full, small, medium, large. auto makes the bottom variant of the sheet responsive to its content, while full makes the sheet take up the full height of the current viewport. small, medium, and large can only be set when the variant is modal. This prop defaults to auto. |
Contents Component
In some cases you man have multiple distinct sections of content to display in the same Sheet. For example, a multi-step dialog where each selection feeds into the next step.
For this, you can use the separate Sheet.Container and Sheet.Contents
components to separate the sheet from each distinct section of content.
() => {
const { isOpen, onOpen, onClose } = useDisclosure();
const [step, setStep] = useState('login-signup-fork');
const LoginSignupFork = () => (
<Sheet.Contents
contentKey="login-signup-fork"
header="Save your favorites"
footer={
<VStack alignItems="stretch" spacing="300">
<Button size="large" onClick={() => setStep('login')}>
Sign up or Log in
</Button>
<Button size="large">Maybe later</Button>
</VStack>
}
>
<Body>Sign up or log in to add this photo to your favorites.</Body>
</Sheet.Contents>
);
const LoginOptions = () => (
<Sheet.Contents contentKey="login" header="Log in to Hover">
<ButtonGroup
variant="tertiary"
size="large"
spacing="300"
orientation="vertical"
width="100%"
mt="400"
>
<Button iconBefore={iconGithub}>Continue with GitHub</Button>
<Button iconBefore={iconFacebook}>Continue with Facebook</Button>
<Link as="button" alignSelf="center" py="200">
<system.span fontWeight="body">or</system.span> continue with
Email
</Link>
</ButtonGroup>
</Sheet.Contents>
);
return (
<>
<Sheet.Container
isOpen={isOpen}
isClosable
onClose={() => {
onClose();
setStep('login-signup-fork');
}}
>
{step === 'login-signup-fork' ? <LoginSignupFork /> : <LoginOptions />}
</Sheet.Container>
<IconButton
icon={iconHeartFilled}
onClick={onOpen}
label="Add Favorite"
/>
</>
);
};Bottom Variant
As mentioned above, Sheet is responsive by default. To always display the
sheet from the bottom of the screen, set variant to "bottom".
() => {
const [size, setSize] = useState('auto');
const { isOpen, onOpen, onClose } = useDisclosure();
const handleOpenClick = size => {
setSize(size);
onOpen();
};
return (
<>
<Sheet
isClosable
isOpen={isOpen}
onClose={onClose}
variant="bottom"
size={size}
>
<Stack
direction={{ base: 'column', tablet: 'row' }}
spacing="500"
pt={{ base: '0', tablet: '300' }}
>
<VStack alignItems="start">
<Heading size={{ base: 600, tablet: 700 }}>Add a photo</Heading>
<LoremIpsum generate={{ p: 1, sentences: 1 }} />
</VStack>
<Image
src="/images/kitten--1920-1080.jpg"
maxHeight="400px"
maxWidth="500px"
/>
</Stack>
</Sheet>
<HStack>
<Button
iconBefore={iconChevronUp}
onClick={() => handleOpenClick('auto')}
>
Open Bottom-only Sheet
</Button>
<Button
iconBefore={iconChevronUp}
onClick={() => handleOpenClick('full')}
>
Open full-size Bottom-only Sheet
</Button>
</HStack>
</>
);
};Modal Variant
To display the Sheet as a Modal in desktop form factors, and bottom sheet in
mobile set variant to "modal".
() => {
const { isOpen, onOpen, onClose } = useDisclosure();
const handleOpenClick = () => {
onOpen();
};
return (
<>
<Sheet isClosable isOpen={isOpen} onClose={onClose} variant="modal">
<Stack
direction={{ base: 'column', tablet: 'row' }}
spacing="500"
pt={{ base: '0', tablet: '300' }}
>
<VStack alignItems="start">
<Heading size={{ base: 600, tablet: 700 }}>Add a photo</Heading>
<LoremIpsum generate={{ p: 1, sentences: 1 }} />
</VStack>
<Image
src="/images/kitten--1920-1080.jpg"
maxHeight="400px"
maxWidth="500px"
/>
</Stack>
</Sheet>
<HStack>
<Button iconBefore={iconExpand} onClick={() => handleOpenClick()}>
Open Modal Sheet
</Button>
</HStack>
</>
);
};Placement
The Sheet takes a placement prop that can be used to control the position of
the drawer on desktop. Mobile placement is always from the bottom of the screen.
It takes the following values:
right- defaultleft
() => {
const { isOpen, onOpen, onClose } = useDisclosure();
const handleOpenClick = () => {
onOpen();
};
return (
<>
<Sheet isClosable isOpen={isOpen} onClose={onClose} placement="left">
<VStack alignItems="start">
<Heading size={{ base: 600, tablet: 700 }}>Add a photo</Heading>
<LoremIpsum generate={{ p: 3, sentences: 3 }} />
</VStack>
</Sheet>
<HStack>
<Button iconBefore={iconExpand} onClick={() => handleOpenClick()}>
Open Left-side Sheet
</Button>
</HStack>
</>
);
};React Native
Sheet is built with Tamagui and provides a bottom sheet interface optimized for iOS and Android platforms.
Usage
import React, { useState } from 'react';
import { Sheet } from '@hoverinc/design-system-react-native';
const App = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<>
<Sheet
isOpen={isOpen}
onOpenChange={setIsOpen}
header="Add a photo"
size="auto"
>
<Stack gap="$400" padding="$600">
<Heading size="md">Upload your image</Heading>
<Body>Choose a photo from your gallery or take a new one.</Body>
</Stack>
</Sheet>
<Button onPress={() => setIsOpen(true)}>Open Sheet</Button>
</>
);
};Size Variants
The Sheet component supports two size variants:
// Auto size - fits content (default)
<Sheet size="auto" header="Auto Size">
<Stack padding="$600">
<Body>This sheet will size to fit its content.</Body>
</Stack>
</Sheet>
// Full size - expands to 100% height
<Sheet size="full" header="Full Size">
<Stack padding="$600">
<Body>This sheet takes the full screen height.</Body>
</Stack>
</Sheet>With Footer
You can add a footer to the sheet for actions:
import React, { useState } from 'react';
import { Button, Sheet, Stack } from '@hoverinc/design-system-react-native';
const App = () => {
const [isOpen, setIsOpen] = useState(false);
return (
<Sheet
isOpen={isOpen}
onOpenChange={setIsOpen}
header="Confirm Action"
footer={
<Stack flexDirection="row" gap="$300" padding="$600">
<Button variant="outline" onPress={() => setIsOpen(false)}>
Cancel
</Button>
<Button onPress={() => setIsOpen(false)}>Confirm</Button>
</Stack>
}
>
<Stack padding="$600">
<Body>Are you sure you want to proceed with this action?</Body>
</Stack>
</Sheet>
);
};Controlled vs Uncontrolled
The Sheet component supports both controlled and uncontrolled usage:
// Controlled
const [isOpen, setIsOpen] = useState(false);
<Sheet isOpen={isOpen} onOpenChange={setIsOpen}>...</Sheet>
// Uncontrolled
<Sheet defaultOpen={false}>...</Sheet>