Sheet
Sheets are surfaces that allow users to view optional information or complete sub-tasks in a workflow while keeping the context of the current page. The most common example of Sheet displays content in a panel that opens from the side of the screen for the user to read or input information. Sheets have default, internal padding for content.
Props
Usage guidelines
- Performing an optional sub-task within a larger task.
- Quick bulk edits on info from a Table.
- Presenting help info while maintaining the current page and its context.
Best practices
Use Sheet for sub-tasks within a large workflow that are optional, like creating a new audience list while creating a campaign.
Use Sheet for quick edits within libraries or tables of content where you expect users to be making multiple edits in one session.
Use the same size Sheet on a product surface. For example, if filling out a form requires multiple Sheets to be opened to complete different subtasks, then all Sheets in that form should be the same width. When in doubt, pick the widest size needed for the entire flow.
Use Sheet for required tasks or main tasks, like logging in. Put those tasks within the content of the page instead.
Use Sheet if edits or sub-tasks require more than two steps. Bring users to a full page experience or consider using Modules to section out content.
Accessibility
Labels
accessibilityDismissButtonLabel
: provides a short, descriptive label for screen readers as a text alternative to the Dismiss button. Populates thearia-label
attribute on the Dismiss button.accessibilitySheetLabel
: provides a short, descriptive label for screen readers to contextualize the purpose of Sheet. Please don’t repeat the same text being passed in the heading prop, but instead provide something that summarizes the Sheet’s purpose. For instance, if theheading
is "Pin Builder", theaccessibilitySheetLabel
can be "Create a new Pin". Populates thearia-label
attribute on the entire dialog.
import React from 'react'; import { Box, Button, Checkbox, CompositeZIndex, Fieldset, FixedZIndex, Flex, Layer, RadioButton, Sheet, Text, TextField, } from 'gestalt'; function SheetWithAccessibilityLabels({ onDismiss }) { return ( <Sheet accessibilityDismissButtonLabel="Close audience creation sheet" accessibilitySheetLabel="Audience list creation for new campaign" heading="Create a new audience list" onDismiss={onDismiss} // eslint-disable-next-line react/no-unstable-nested-components footer={({ onDismissStart }) => ( <Flex alignItems="center" justifyContent="end"> <Button color="red" text="Create" onClick={onDismissStart} /> </Flex> )} size="md" > <Flex direction="column" gap={{ row: 0, column: 12, }} > <Flex direction="column" gap={{ row: 0, column: 4, }} > <Box> <Text inline weight="bold"> Step 1: </Text> <Text inline> Audience list details</Text> </Box> <TextField id="audience-name" label="Audience name" placeholder="Name your audience" onChange={() => {}} /> <TextField id="desc" label="Audience description" placeholder="Describe your audience" onChange={() => {}} /> <Fieldset legend="When adding this audience list to an ad group:"> <Flex direction="column" gap={{ row: 0, column: 3, }} > <RadioButton label="Include list" name="audience" value="include" onChange={() => {}} id="include" /> <RadioButton label="Exclude list" name="audience" value="include" onChange={() => {}} id="exclude" /> </Flex> </Fieldset> </Flex> <Flex direction="column" gap={{ row: 0, column: 4, }} > <Box> <Text inline weight="bold"> Step 2: </Text> <Text inline> Select conversion source</Text> </Box> <Text> To use a conversion source other than a Pinterest Tag, add a filter and configure the source of this event. </Text> <Fieldset legend="Select conversion source:" legendDisplay="hidden"> <Flex direction="column" gap={{ row: 0, column: 3, }} > <RadioButton label="Pinterest Tag" name="source" value="pin" onChange={() => {}} id="tag" /> <RadioButton label="Mobile Measurement Partners (MMP)" name="source" value="mmp" onChange={() => {}} id="mmp" /> <RadioButton label="Conversion Upload" name="source" value="conversion" onChange={() => {}} id="upload" /> <RadioButton label="API" name="source" value="api" onChange={() => {}} id="api" /> </Flex> </Fieldset> </Flex> <Flex direction="column" gap={{ row: 0, column: 4, }} > <Box> <Text inline weight="bold"> Step 3: </Text> <Text inline> Set a filter</Text> </Box> <TextField id="users" label="Users in the past few days" placeholder="Ex. 4" onChange={() => {}} /> <Checkbox label="Include past traffic data" name="traffic" id="traffic" onChange={() => {}} /> </Flex> </Flex> </Sheet> ); } export default function AccessibilityExample() { const [shouldShow, setShouldShow] = React.useState(true); const HEADER_ZINDEX = new FixedZIndex(10); const sheetZIndex = new CompositeZIndex([HEADER_ZINDEX]); return ( <React.Fragment> <Box padding={8}> <Button text="View example Sheet" onClick={() => setShouldShow(true)} /> </Box> {shouldShow && ( <Layer zIndex={sheetZIndex}> <SheetWithAccessibilityLabels onDismiss={() => setShouldShow(false)} /> </Layer> )} </React.Fragment> ); }
Focus management
When Sheet opens, focus should be placed on the first interactive element within the Sheet. When Sheet is closed, focus should be placed back on the button that triggered the Sheet.
Localization
Be sure to localize the heading
, accessibilityDismissButtonLabel
and accessibilitySheetLabel
props. Note that localization can lengthen text by 20 to 30 percent.
Variants
Heading
As a default, Sheet consists of a heading
and content passed as children
. The heading
of Sheet will have a drop shadow when content scrolls under it.
import React from 'react'; import { Box, Button, Checkbox, CompositeZIndex, Fieldset, FixedZIndex, Flex, Layer, RadioButton, Sheet, Text, TextField, } from 'gestalt'; function SheetWithAccessibilityLabels({ onDismiss }) { return ( <Sheet accessibilityDismissButtonLabel="Close audience creation sheet" accessibilitySheetLabel="Audience list creation for new campaign" heading="Create a new audience list" onDismiss={onDismiss} // eslint-disable-next-line react/no-unstable-nested-components footer={({ onDismissStart }) => ( <Flex alignItems="center" justifyContent="end"> <Button color="red" text="Create" onClick={onDismissStart} /> </Flex> )} size="md" > <Flex direction="column" gap={{ row: 0, column: 12, }} > <Flex direction="column" gap={{ row: 0, column: 4, }} > <Box> <Text inline weight="bold"> Step 1: </Text> <Text inline> Audience list details</Text> </Box> <TextField id="audience-name" label="Audience name" placeholder="Name your audience" onChange={() => {}} /> <TextField id="desc" label="Audience description" placeholder="Describe your audience" onChange={() => {}} /> <Fieldset legend="When adding this audience list to an ad group:"> <Flex direction="column" gap={{ row: 0, column: 3, }} > <RadioButton label="Include list" name="audience" value="include" onChange={() => {}} id="include" /> <RadioButton label="Exclude list" name="audience" value="include" onChange={() => {}} id="exclude" /> </Flex> </Fieldset> </Flex> <Flex direction="column" gap={{ row: 0, column: 4, }} > <Box> <Text inline weight="bold"> Step 2: </Text> <Text inline> Select conversion source</Text> </Box> <Text> To use a conversion source other than a Pinterest Tag, add a filter and configure the source of this event. </Text> <Fieldset legend="Select conversion source:" legendDisplay="hidden"> <Flex direction="column" gap={{ row: 0, column: 3, }} > <RadioButton label="Pinterest Tag" name="source" value="pin" onChange={() => {}} id="tag" /> <RadioButton label="Mobile Measurement Partners (MMP)" name="source" value="mmp" onChange={() => {}} id="mmp" /> <RadioButton label="Conversion Upload" name="source" value="conversion" onChange={() => {}} id="upload" /> <RadioButton label="API" name="source" value="api" onChange={() => {}} id="api" /> </Flex> </Fieldset> </Flex> <Flex direction="column" gap={{ row: 0, column: 4, }} > <Box> <Text inline weight="bold"> Step 3: </Text> <Text inline> Set a filter</Text> </Box> <TextField id="users" label="Users in the past few days" placeholder="Ex. 4" onChange={() => {}} /> <Checkbox label="Include past traffic data" name="traffic" id="traffic" onChange={() => {}} /> </Flex> </Flex> </Sheet> ); } export default function AccessibilityExample() { const [shouldShow, setShouldShow] = React.useState(true); const HEADER_ZINDEX = new FixedZIndex(10); const sheetZIndex = new CompositeZIndex([HEADER_ZINDEX]); return ( <React.Fragment> <Box padding={8}> <Button text="View example Sheet" onClick={() => setShouldShow(true)} /> </Box> {shouldShow && ( <Layer zIndex={sheetZIndex}> <SheetWithAccessibilityLabels onDismiss={() => setShouldShow(false)} /> </Layer> )} </React.Fragment> ); }
Sub-heading
A subHeading
is a container that can be used for additional navigation or sub-text. The sub-heading sits at the top under the heading, and will always remain visible if the content scrolls.
import React from 'react'; import { Box, Button, CompositeZIndex, FixedZIndex, Flex, Layer, Sheet, Tabs, Text, } from 'gestalt'; function SheetWithSubheading({ onDismiss }) { const [activeTabIndex, setActiveTabIndex] = React.useState(0); const enRef = React.useRef(); const esRef = React.useRef(); const ptRef = React.useRef(); const chRef = React.useRef(); const refs = [enRef, esRef, ptRef, chRef]; const handleChangeTab = ({ activeTabIndex: activeTabIndexLocal, event }) => { event.preventDefault(); setActiveTabIndex(activeTabIndexLocal); refs[activeTabIndexLocal].current?.scrollIntoView({ behavior: 'smooth', }); }; return ( <Sheet accessibilityDismissButtonLabel="Close" accessibilitySheetLabel="Example sheet to demonstrate subHeading" heading="Sheet with subHeading" onDismiss={onDismiss} footer={ <Flex justifyContent="end"> <Button color="red" text="Apply changes" /> </Flex> } size="md" subHeading={ <Box marginBottom={4} marginStart={8} marginEnd={8}> <Tabs tabs={[ { text: 'English', href: '#', }, { text: 'Español', href: '#', }, { text: 'Português', href: '#', }, { text: '普通话', href: '#', }, ]} activeTabIndex={activeTabIndex} onChange={handleChangeTab} /> </Box> } > <Box marginBottom={8} ref={enRef}> <Text weight="bold">English</Text> <Text> <ol> <li>One</li> <li>Two</li> <li>Three</li> <li>Four</li> <li>Five</li> <li>Six</li> <li>Seven</li> <li>Eight</li> <li>Nine</li> <li>Ten</li> </ol> </Text> </Box> <Box marginBottom={8} ref={esRef}> <Text weight="bold">Español</Text> <Text> <ol> <li>Uno</li> <li>Dos</li> <li>Tres</li> <li>Cuatro</li> <li>Cinco</li> <li>Seis</li> <li>Siete</li> <li>Ocho</li> <li>Nueve</li> <li>Diez</li> </ol> </Text> </Box> <Box marginBottom={8} ref={ptRef}> <Text weight="bold">Português</Text> <Text> <ol> <li>Um</li> <li>Dois</li> <li>Três</li> <li>Quatro</li> <li>Cinco</li> <li>Seis</li> <li>Sete</li> <li>Oito</li> <li>Nove</li> <li>Dez</li> </ol> </Text> </Box> <Box marginBottom={8} ref={chRef}> <Text weight="bold">普通话</Text> <Text> <ol> <li>一</li> <li>二</li> <li>三</li> <li>四</li> <li>五</li> <li>六</li> <li>七</li> <li>八</li> <li>九</li> <li>十</li> </ol> </Text> </Box> </Sheet> ); } export default function SubheadingExample() { const [shouldShow, setShouldShow] = React.useState(true); const HEADER_ZINDEX = new FixedZIndex(10); const sheetZIndex = new CompositeZIndex([HEADER_ZINDEX]); return ( <React.Fragment> <Box padding={8}> <Button text="View subheading example" onClick={() => setShouldShow(true)} /> </Box> {shouldShow && ( <Layer zIndex={sheetZIndex}> <SheetWithSubheading onDismiss={() => setShouldShow(false)} /> </Layer> )} </React.Fragment> ); }
The footer
is used for Sheet tasks that require additional actions, such as submitting or deleting information.
import React from 'react'; import { Box, Button, CompositeZIndex, Fieldset, FixedZIndex, Flex, Layer, Module, RadioButton, Sheet, Text, } from 'gestalt'; function SheetWithFooter({ onDismiss }) { return ( <Sheet accessibilityDismissButtonLabel="Close" accessibilitySheetLabel="Bulk edit for 5 ad groups of Nordstrom Account" heading="Editing 5 ad groups" onDismiss={onDismiss} // eslint-disable-next-line react/no-unstable-nested-components footer={({ onDismissStart }) => ( <Flex alignItems="center" justifyContent="between"> <Button color="transparent" text="Delete" /> <Button color="red" text="Apply changes" onClick={onDismissStart} /> </Flex> )} size="md" > <Flex direction="column" gap={{ row: 0, column: 8, }} > <Text weight="bold">Bids</Text> <Flex gap={{ row: 4, column: 0, }} > <Text> Adjust bids for the selected ad groups below. Changes made here will apply to all selected ad groups. </Text> <Flex.Item flex="none"> <Button text="Reset bids" disabled /> </Flex.Item> </Flex> <Module.Expandable accessibilityExpandLabel="Expand the module" accessibilityCollapseLabel="Collapse the module" id="ModuleExample - default" expandedIndex={0} items={[ { children: ( <Fieldset legend="What bid campaign do you want to run?" legendDisplay="hidden" > <Flex direction="column" gap={{ row: 0, column: 2, }} > <RadioButton checked id="favoriteDog" label="No change" name="favorite" onChange={() => {}} value="dogs" /> <RadioButton checked={false} id="favoriteCat" label="Automatic (recommended)" subtext="Pinterest aims to get the most clicks for your budget" name="favorite" onChange={() => {}} value="cats" /> <RadioButton checked={false} id="favoritePlants" label="Custom" subtext="You control how much to bid at auctions" name="favorite" onChange={() => {}} value="plants" /> </Flex> </Fieldset> ), summary: ['Custom'], title: 'Bid', }, ]} /> <Module.Expandable accessibilityExpandLabel="Expand the module" accessibilityCollapseLabel="Collapse the module" id="ModuleExample - preview" items={[ { children: <Text> Preview table of changes here</Text>, summary: ['5 ad groups changing'], title: 'Preview bid changes', }, ]} /> </Flex> </Sheet> ); } export default function FooterExample() { const [shouldShow, setShouldShow] = React.useState(true); const HEADER_ZINDEX = new FixedZIndex(10); const sheetZIndex = new CompositeZIndex([HEADER_ZINDEX]); return ( <React.Fragment> <Box padding={8}> <Button text="View footer example" onClick={() => setShouldShow(true)} /> </Box> {shouldShow && ( <Layer zIndex={sheetZIndex}> <SheetWithFooter onDismiss={() => setShouldShow(false)} /> </Layer> )} </React.Fragment> ); }
Sizes
Sheet comes in 3 sizes: small (sm
), medium (md
), and large (lg
).
- Small Sheets (540px) are primarily used for displaying information or acting as a point to link to other content. They are the least commonly used.
- Medium Sheets (720px) are the standard size offered for content.
- Large Sheets (900px) should be used in cases where there may be columns of content or navigation where the additional space is required to keep the content at a comfortable reading width.
import React from 'react'; import { Box, Button, CompositeZIndex, Fieldset, FixedZIndex, Flex, Layer, Module, RadioButton, Sheet, Text, } from 'gestalt'; export default function SizesExample() { function reducer(state, action) { switch (action.type) { case 'small': return { heading: 'Small sheet', size: 'sm' }; case 'medium': return { heading: 'Medium sheet', size: 'md' }; case 'large': return { heading: 'Large sheet', size: 'lg' }; case 'none': return {}; default: throw new Error(); } } const initialState = {}; const [state, dispatch] = React.useReducer(reducer, initialState); const HEADER_ZINDEX = new FixedZIndex(10); const sheetZIndex = new CompositeZIndex([HEADER_ZINDEX]); return ( <React.Fragment> <Box padding={8}> <Box padding={1}> <Button text="Small Sheet" onClick={() => { dispatch({ type: 'small' }); }} /> </Box> <Box padding={1}> <Button text="Medium Sheet" onClick={() => { dispatch({ type: 'medium' }); }} /> </Box> <Box padding={1}> <Button text="Large Sheet" onClick={() => { dispatch({ type: 'large' }); }} /> </Box> </Box> {state.size && ( <Layer zIndex={sheetZIndex}> <Sheet accessibilityDismissButtonLabel="Dismiss" accessibilitySheetLabel="Example sheet to demonstrate different sizes" footer={ <Flex justifyContent="end"> <Button text="Apply changes" color="red" /> </Flex> } heading={state.heading} onDismiss={() => { dispatch({ type: 'none' }); }} size={state.size} > <Flex direction="column" gap={{ row: 0, column: 8, }} > <Text weight="bold">Bids</Text> <Flex gap={{ row: 4, column: 0, }} > <Text> Adjust bids for the selected ad groups below. Changes made here will apply to all selected ad groups. </Text> <Flex.Item flex="none"> <Button text="Reset bids" disabled /> </Flex.Item> </Flex> <Module.Expandable accessibilityExpandLabel="Expand the module" accessibilityCollapseLabel="Collapse the module" id="ModuleExample - default" expandedIndex={0} items={[ { children: ( <Fieldset legend="What bid campaign do you want to run?" legendDisplay="hidden" > <Flex direction="column" gap={{ row: 0, column: 2, }} > <RadioButton checked id="favoriteDog" label="No change" name="favorite" onChange={() => {}} value="dogs" /> <RadioButton checked={false} id="favoriteCat" label="Automatic (recommended)" subtext="Pinterest aims to get the most clicks for your budget" name="favorite" onChange={() => {}} value="cats" /> <RadioButton checked={false} id="favoritePlants" label="Custom" subtext="You control how much to bid at auctions" name="favorite" onChange={() => {}} value="plants" /> </Flex> </Fieldset> ), summary: ['Custom'], title: 'Bid', }, ]} /> <Module.Expandable accessibilityExpandLabel="Expand the module" accessibilityCollapseLabel="Collapse the module" id="ModuleExample - preview" items={[ { children: <Text> Preview table of changes here</Text>, summary: ['5 ad groups changing'], title: 'Preview bid changes', }, ]} /> </Flex> </Sheet> </Layer> )} </React.Fragment> ); }
Preventing close on outside click
By default, users can click outside Sheet (on the overlay) to close it. This can be disabled by setting closeOnOutsideClick
to false. This may be implemented in order to prevent users from accidentally clicking out of the Sheet and losing information they’ve entered. The ESC
key can still be used to close the Sheet.
import React from 'react'; import { Box, Button, Checkbox, CompositeZIndex, Fieldset, FixedZIndex, Flex, Layer, RadioButton, Sheet, Text, TextField, } from 'gestalt'; function SheetWithoutOutsideClick({ onDismiss }) { return ( <Sheet accessibilityDismissButtonLabel="Close" accessibilitySheetLabel="Example sheet for demonstration" heading="Create new audience list" closeOnOutsideClick={false} onDismiss={onDismiss} // eslint-disable-next-line react/no-unstable-nested-components footer={({ onDismissStart }) => ( <Flex alignItems="center" justifyContent="end"> <Button color="red" text="Create" onClick={onDismissStart} /> </Flex> )} size="md" > <Flex direction="column" gap={{ row: 0, column: 12, }} > <Flex direction="column" gap={{ row: 0, column: 4, }} > <Box> <Text inline weight="bold"> Step 1: </Text> <Text inline> Audience list details</Text> </Box> <TextField label="Audience name" placeholder="Name your audience" id="name-your-audience" onChange={() => {}} /> <TextField label="Audience description" placeholder="Describe your audience" id="describe-your-audience" onChange={() => {}} /> <Fieldset legend="When adding this audience list to an ad group:"> <Flex direction="column" gap={{ row: 0, column: 3, }} > <RadioButton id="include-list" label="Include list" name="audience" value="include" onChange={() => {}} /> <RadioButton id="exclude-list" label="Exclude list" name="audience" value="include" onChange={() => {}} /> </Flex> </Fieldset> </Flex> <Flex direction="column" gap={{ row: 0, column: 4, }} > <Box> <Text inline weight="bold"> Step 2: </Text> <Text inline> Select conversion source</Text> </Box> <Text> To use a conversion source other than a Pinterest Tag, add a filter and configure the source of this event. </Text> <Fieldset legend="Select conversion source:" legendDisplay="hidden"> <Flex direction="column" gap={{ row: 0, column: 3, }} > <RadioButton id="pinterest-tag" label="Pinterest Tag" name="source" value="pin" onChange={() => {}} /> <RadioButton id="mobile-measurement" label="Mobile Measurement Partners (MMP)" name="source" value="mmp" onChange={() => {}} /> <RadioButton id="conversion-upload" label="Conversion Upload" name="source" value="conversion" onChange={() => {}} /> <RadioButton id="api" label="API" name="source" value="api" onChange={() => {}} /> </Flex> </Fieldset> </Flex> <Flex direction="column" gap={{ row: 0, column: 4, }} > <Box> <Text inline weight="bold"> Step 3: </Text> <Text inline> Set a filter</Text> </Box> <TextField id="past-users" onChange={() => {}} label="Users in the past few days" placeholder="Ex. 4" /> <Checkbox id="traffic" label="Include past traffic data" name="traffic" onChange={() => {}} /> </Flex> </Flex> </Sheet> ); } export default function PreventClosingExample() { const [shouldShow, setShouldShow] = React.useState(false); const HEADER_ZINDEX = new FixedZIndex(10); const sheetZIndex = new CompositeZIndex([HEADER_ZINDEX]); return ( <React.Fragment> <Box padding={8}> <Button text="View Sheet" onClick={() => setShouldShow(true)} /> </Box> {shouldShow && ( <Layer zIndex={sheetZIndex}> <SheetWithoutOutsideClick onDismiss={() => setShouldShow(false)} /> </Layer> )} </React.Fragment> ); }
Animation
By default, Sheet animates in, with the initial render process from the entry-point, and out, when the ESC
key is pressed, the header close button is pressed, or the user clicks outside of the Sheet. However, to trigger the exit-animation from other elements within the children
or footer
, the following render prop can be used:
({ onDismissStart }) => ( ... )
When using this render prop, just pass the argument onDismissStart
to your exit-point action elements. In the example below, we've added the exit animation to the:
- Close button (subHeading)
- Right arrow icon red button (children)
- Done red button (children)
- Left arrow red icon button (children)
- Close button (footer)
Sheet also provides onAnimationEnd
, a callback that gets triggered at the end of each animation. The callback has access to animationState
to identify the end of each 'in' and 'out' animation for cases where the two events trigger different responses..
import React from 'react'; import { Box, Button, CompositeZIndex, FixedZIndex, Flex, IconButton, Layer, Sheet, } from 'gestalt'; export default function AnimationExample() { const [shouldShow, setShouldShow] = React.useState(false); const HEADER_ZINDEX = new FixedZIndex(10); const sheetZIndex = new CompositeZIndex([HEADER_ZINDEX]); return ( <React.Fragment> {' '} <Box padding={8}> <Button text="Open example sheet" onClick={() => setShouldShow(true)} /> </Box> {shouldShow && ( <Layer zIndex={sheetZIndex}> <Sheet accessibilityDismissButtonLabel="Close" accessibilitySheetLabel="Animated sheet" // eslint-disable-next-line react/no-unstable-nested-components footer={({ onDismissStart }) => ( <Flex justifyContent="end"> <Button onClick={onDismissStart} text="Close on Footer" /> </Flex> )} heading="Animated Sheet" onDismiss={() => setShouldShow(false)} size="md" // eslint-disable-next-line react/no-unstable-nested-components subHeading={({ onDismissStart }) => ( <Box marginBottom={4} marginStart={8} marginEnd={8}> <Button color="blue" onClick={onDismissStart} text="Close on Sub-heading" /> </Box> )} > {({ onDismissStart }) => ( <Flex justifyContent="center" alignItems="center" height="100%"> <IconButton accessibilityLabel="Done icon left" icon="directional-arrow-right" iconColor="red" onClick={onDismissStart} size="lg" /> <Button color="red" onClick={onDismissStart} size="lg" text="Done on Children" /> <IconButton accessibilityLabel="Done icon right" icon="directional-arrow-left" iconColor="red" onClick={onDismissStart} size="lg" /> </Flex> )} </Sheet> </Layer> )} </React.Fragment> ); }
Component quality checklist
Quality item | Status | Status description |
---|---|---|
Figma Library | Planned | Component is slotted to be added to Figma. |
Responsive Web | Ready | Component is available in code for web and mobile web. |
iOS | Component is not currently available in code for iOS. | |
Android | Component is not currently available in code for Android. |