SideSheet
Side sheets show secondary content at the side of the screen, such as filters or the details of a selected item. Follows the Material Design 3 Side sheet specification.
The sheet is 256dp wide. It has a titleLarge headline, an optional back button, a close button,
content that scrolls, and an optional row of actions at the bottom.
The modal variant renders through Portal, so your app must have a PortalHost
once at the root. It uses the PORTAL_LAYERS.sheet layer, the same as the bottom sheet and the
navigation drawer.
Usage
Pass the headline as headline, the content as children, and the buttons as actions. The
close button shows when the sheet has an onDismiss.
import { Button, Chip, PortalHost, SideSheet } from '@rootnative/components'
import { mdiResolver } from '@rootnative/components/mdi'
import { ThemeProvider } from '@rootnative/core'
import { SafeAreaProvider } from 'react-native-safe-area-context'
import { useState } from 'react'
import { Text, View } from 'react-native'
const GENRES = ['Drama', 'Comedy', 'Documentary']
function Screen() {
const [open, setOpen] = useState(false)
const [genres, setGenres] = useState(['Drama'])
const toggle = (genre) =>
setGenres((current) =>
current.includes(genre) ? current.filter((g) => g !== genre) : [...current, genre],
)
return (
<View style={{ flex: 1, padding: 24, gap: 16 }}>
<Button variant="tonal" onPress={() => setOpen(true)}>Filters</Button>
<Text>Genres: {genres.join(', ')}</Text>
<SideSheet
visible={open}
onDismiss={() => setOpen(false)}
headline="Filters"
actions={
<>
<Button onPress={() => setOpen(false)}>Save</Button>
<Button variant="outlined" onPress={() => setOpen(false)}>Cancel</Button>
</>
}
>
<View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 8 }}>
{GENRES.map((genre) => (
<Chip
key={genre}
variant="filter"
selected={genres.includes(genre)}
onPress={() => toggle(genre)}
>
{genre}
</Chip>
))}
</View>
</SideSheet>
</View>
)
}
export default function App() {
return (
<SafeAreaProvider>
<ThemeProvider iconResolver={mdiResolver}>
<PortalHost>
<Screen />
</PortalHost>
</ThemeProvider>
</SafeAreaProvider>
)
}
The content scrolls between the header and the actions. The actions stay at the bottom of the sheet.
Variants
| Variant | Where it renders | Container | Dismissal |
|---|---|---|---|
'modal' (default) | Through Portal, over the content, from the end edge behind a 32% scrim | surfaceContainerLow, elevation level 1, 16dp corners on the inner edge | Close button, scrim press, Android back, and Escape on web |
'standard' | In place, beside the main content, which stays interactive | surface, elevation level 0, no corners | Close button, when you pass onDismiss |
A modal sheet needs visible and onDismiss. On a standard sheet, onDismiss is optional and
only shows the close button. The type rejects visible, dismissable, edge, hostName,
scrimStyle and scrimAccessibilityLabel on a standard sheet.
Put a standard sheet in a row, beside the content. It stretches to the height of the row. Add
a vertical Divider when the sheet and the content have the same color:
<View style={{ flex: 1, flexDirection: 'row' }}>
<View style={{ flex: 1 }}>{/* main content */}</View>
<Divider orientation="vertical" />
{showDetails ? (
<SideSheet variant="standard" headline="Details" onDismiss={() => setShowDetails(false)}>
<Details />
</SideSheet>
) : null}
</View>
A standard sheet has no animation. It shows and hides when you add it to the row or remove it.
Header
The header holds three optional parts, in this order:
- Back button.
onBackshows it and calls it on a press. Use it when the sheet shows a second level of content. The icon isarrow-left, andarrow-rightin RTL.backIconreplaces it. - Headline.
headlineis atitleLargetext. On web it renders as<h2>. SetheadingLevelto fit the outline of the page. - Close button.
onDismissshows it. On a modal sheet it callsonDismissalso whendismissableisfalse, because it is an explicit control.closeIconreplaces thecloseicon.
With no headline, no onBack and no onDismiss, the sheet has no header.
<SideSheet
visible={open}
onDismiss={close}
headline={level === 'sort' ? 'Sort order' : 'Settings'}
onBack={level === 'sort' ? () => setLevel('settings') : undefined}
>
{level === 'sort' ? <SortOrders /> : <Settings />}
</SideSheet>
Actions
actions is a row of buttons below the content. MD3 shows a filled button and an outlined
button. A divider separates the row from the content. Set showActionsDivider={false} to
remove it.
Edge, detached, and width
edge(modal only) sets the screen edge that the sheet enters from.'end'(the default) is the right edge in LTR and the left edge in RTL.'start'is the other edge.detachedadds a 16dp margin on all four sides and rounds all four corners.widthsets the width of the sheet. The default is 256dp. Set the width with this prop, not withstyle: the slide distance of a modal sheet comes fromwidth. On a narrow window, a modal sheet shrinks, so a 56dp strip of scrim stays visible.
<SideSheet visible={open} onDismiss={close} headline="Account" edge="start" detached width={320}>
<AccountDetails />
</SideSheet>
Safe area
A modal sheet spans the full height of the screen. By default, it pads its content by the top
and bottom insets, and the container color extends under them. A standard sheet sets both off
by default, because the Layout around it usually owns the edges. insetTop and
insetBottom override either default.
Overrides
| Prop | Target |
|---|---|
containerColor | The sheet background |
contentColor | The headline and the back and close icons. The hover and press layers of the two buttons derive from it |
headlineStyle | The headline Text |
style | The sheet surface |
scrimStyle | The scrim (modal only) |
When you pass testID, the sheet surface carries it. A modal sheet's scrim carries
<testID>-scrim, and the layer that slides carries <testID>-layer.
Tokens
| Token | Value |
|---|---|
| Sheet | 256dp wide. Modal: surfaceContainerLow at level 1, 16dp corners on the inner edge. Standard: surface at level 0, no corners |
| Detached | 16dp margin, 16dp corners on all four sides |
| Scrim | scrim at 32% |
| Headline | titleLarge, onSurface |
| Back and close icons | 24dp, onSurfaceVariant, in a 40dp icon button |
| Header | At least 72dp tall. The text starts 24dp from the sheet edge, and an icon button 16dp |
| Content | 24dp padding at the sides and the bottom |
| Actions | 24dp padding at the sides, 16dp at the top and the bottom, 8dp between buttons, below a 1dp outlineVariant divider |
Accessibility
- A modal sheet is a
dialogwitharia-modal. A standard sheet is acomplementaryregion, which renders as<aside>on web. - The headline names the sheet. Pass
accessibilityLabelwhen the sheet has no headline, or to give it a different name. - The headline is a
heading. - The back and close buttons are named "Back" and "Close".
backAccessibilityLabelandcloseAccessibilityLabelchange the names. The scrim is named "Close side sheet". - On web, a modal sheet traps keyboard focus: focus moves into it when it opens, Tab cycles
inside it, Escape dismisses it when
dismissable, and focus goes back to the trigger when it closes. - On Android, the hardware back button dismisses a
dismissablemodal sheet.
Props
| Prop | Type | Default | Required | Description |
|---|---|---|---|---|
children | ReactNode | - | No | Sheet content. It scrolls between the header and the actions. |
headline | string | - | No | Headline in `titleLarge` at the top of the sheet. It is also the accessible name of the sheet when `accessibilityLabel` is not set. |
headingLevel | enum | 2 | No | The outline level of the headline on web, where it renders as `<hN>`. Native has no heading levels and ignores it. |
onBack | () => void | - | No | Shows a back button before the headline and calls this on press. Use it when the sheet shows a second level of content. |
backIcon | IconSource | 'arrow-left' ('arrow-right' in RTL) | No | Icon for the back button. |
backAccessibilityLabel | string | 'Back' | No | Screen-reader label for the back button. |
closeIcon | IconSource | 'close' | No | Icon for the close button. The close button shows when the sheet has an `onDismiss`. |
closeAccessibilityLabel | string | 'Close' | No | Screen-reader label for the close button. |
actions | ReactNode | - | No | Action buttons in a row below the content, such as a filled "Save" and an outlined "Cancel". The row stays in place while the content scrolls. |
showActionsDivider | boolean | true | No | Whether a divider separates the actions from the content. Ignored without `actions`. |
detached | boolean | false | No | Whether the sheet is detached from the screen edges by a 16dp margin, with all four corners rounded. |
width | number | 256 | No | Width of the sheet in dp. A modal sheet shrinks on a narrow window, so a strip of scrim stays visible. Set the width here and not in `style`: the slide distance of a modal sheet comes from this value. |
insetTop | boolean | true for 'modal', false for 'standard' | No | Whether the sheet's content is padded by the top safe-area inset. The container color still extends under the inset. |
insetBottom | boolean | true for 'modal', false for 'standard' | No | Whether the sheet's content is padded by the bottom safe-area inset. |
containerColor | string | surfaceContainerLow for 'modal', surface for 'standard' | No | Override the sheet background. |
contentColor | string | onSurface for the headline, onSurfaceVariant for the icons | No | Override the color of the headline and of the back and close icons. The hover and press layers of the two buttons derive from it. |
headlineStyle | StyleProp<TextStyle> | - | No | Style applied to the headline `Text`. |
style | StyleProp<ViewStyle> | - | No | Style applied to the sheet surface. |
accessibilityLabel | string | - | No | Screen-reader label for the sheet. Defaults to `headline`. A modal sheet is announced as a dialog, so it needs a name. |
testID | string | - | No | Test id applied to the sheet surface. A modal sheet's scrim carries `<testID>-scrim`, and the layer that slides carries `<testID>-layer`. |
variant | enum | 'modal' | No | - |
visible | boolean | - | No | Whether the sheet is shown. Exit animations run before it unmounts. |
onDismiss | (() => void) | (() => void) | - | No | Called when the user dismisses the sheet: the close button, a scrim press, the Android back button, or Escape on web. Set `visible` to `false` in response. Called by the close button. Omit it for a sheet with no close button. Remove the sheet from the layout in response. |
dismissable | boolean | true | No | Whether the scrim press, the Android back button, and Escape dismiss the sheet. The close button always calls `onDismiss`. |
edge | enum | 'end' | No | Screen edge the sheet enters from. `'end'` is the right edge in LTR and the left edge in RTL. |
hostName | string | - | No | Render into a specific named `PortalHost`. |
scrimStyle | any | - | No | Style applied to the scrim. |
scrimAccessibilityLabel | string | 'Close side sheet' | No | Screen-reader label for the scrim's dismiss action. |