Blueprint

Sheet

Sheet is part of a collection of components, also including Modal, Dialog and Drawer, that overlay content on top of a full-page view and often block the UI below. A Modal presents content, tasks, or actions that require exclusive user focus.

Optimized for Mobile

A Sheet is automatically displayed from either the bottom of the screen or the right side depending on the size of the screen. On mobile devices, the Sheet is dismissible with gestures in addition to external taps and the close button.

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.

live

() => {
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
<Sheet
footer={
<VStack alignItems="stretch" spacing="300">
<Button size="large">Sign up or Log in</Button>
<Button fill="minimal" 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={iSmartphone} onClick={onOpen} shape="box">
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.

Note

Because it usually only makes sense to restrict the height on mobile, passing a single value such as 40vh will only be applied on mobile by default. To also restrict the height when the Sheet is displayed on the side of the screen pass a responsive value like { base: '40vh', tablet: '50vh' }.

live

() => {
const { isOpen, onOpen, onClose } = useDisclosure();
return (
<>
<Sheet
footer={
<VStack alignItems="stretch" spacing="300">
<Button size="large" onClick={onClose}>
Accept
</Button>
<Button fill="minimal" size="large" onClick={onClose}>
Cancel
</Button>
</VStack>
}
header="Terms & Privacy"
isClosable
isOpen={isOpen}
onClose={onClose}
maxContentHeight="40vh"
>
<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={hUserBusiness} onClick={onOpen} shape="box">
Terms & Privacy
</Button>
</>
);
};


PropDescription
headerSets 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.
footerSets 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.
isClosableWhether to display the close button. Defaults to true.
sizeSize of the Sheet component; which can either be auto or full. 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. 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.

Content Key

Setting a unique contentKey on each Sheet.Contents component enables animation between the different sheet contents. When setting contentKey be sure to either set it on either all or none of the Sheet.Contents components that will be displayed in the same sheet.

live

() => {
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 fill="minimal" 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={iGithub}>Continue with GitHub</Button>
<Button iconBefore={iGitlab}>Continue with GitLab</Button>
<Button iconBefore={iFacebook}>Continue with Facebook</Button>
<Button iconBefore={iDribbble}>Continue with Dribbble</Button>
<Link as="button" alignSelf="center" py="200">
<system.span fontWeight="body">or</system.span>&nbsp;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={iHeart} 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".

live

() => {
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={iArrowUp}
onClick={() => handleOpenClick('auto')}
shape="box"
>
Open Bottom-only Sheet
</Button>
<Button
iconBefore={iArrowUp}
onClick={() => handleOpenClick('full')}
shape="box"
>
Open full-size Bottom-only Sheet
</Button>
</HStack>
</>
);
};

To display the Sheet as a Modal in desktop form factors, and bottom sheet in mobile set variant to "modal".

live

() => {
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={iMaximize}
onClick={() => handleOpenClick()}
shape="box"
>
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 - default
  • left
live

() => {
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={iMaximize}
onClick={() => handleOpenClick()}
shape="box"
>
Open Left-side Sheet
</Button>
</HStack>
</>
);
};


Copyright © 2024 Hover Inc. All Rights Reserved.