Skip to main content

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​

VariantWhere it rendersContainerDismissal
'modal' (default)Through Portal, over the content, from the end edge behind a 32% scrimsurfaceContainerLow, elevation level 1, 16dp corners on the inner edgeClose button, scrim press, Android back, and Escape on web
'standard'In place, beside the main content, which stays interactivesurface, elevation level 0, no cornersClose 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.

The header holds three optional parts, in this order:

  • Back button. onBack shows it and calls it on a press. Use it when the sheet shows a second level of content. The icon is arrow-left, and arrow-right in RTL. backIcon replaces it.
  • Headline. headline is a titleLarge text. On web it renders as <h2>. Set headingLevel to fit the outline of the page.
  • Close button. onDismiss shows it. On a modal sheet it calls onDismiss also when dismissable is false, because it is an explicit control. closeIcon replaces the close icon.

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.
  • detached adds a 16dp margin on all four sides and rounds all four corners.
  • width sets the width of the sheet. The default is 256dp. Set the width with this prop, not with style: the slide distance of a modal sheet comes from width. 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​

PropTarget
containerColorThe sheet background
contentColorThe headline and the back and close icons. The hover and press layers of the two buttons derive from it
headlineStyleThe headline Text
styleThe sheet surface
scrimStyleThe 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​

TokenValue
Sheet256dp wide. Modal: surfaceContainerLow at level 1, 16dp corners on the inner edge. Standard: surface at level 0, no corners
Detached16dp margin, 16dp corners on all four sides
Scrimscrim at 32%
HeadlinetitleLarge, onSurface
Back and close icons24dp, onSurfaceVariant, in a 40dp icon button
HeaderAt least 72dp tall. The text starts 24dp from the sheet edge, and an icon button 16dp
Content24dp padding at the sides and the bottom
Actions24dp 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 dialog with aria-modal. A standard sheet is a complementary region, which renders as <aside> on web.
  • The headline names the sheet. Pass accessibilityLabel when 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". backAccessibilityLabel and closeAccessibilityLabel change 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 dismissable modal sheet.

Props​

PropTypeDefaultRequiredDescription
childrenReactNode-NoSheet content. It scrolls between the header and the actions.
headlinestring-NoHeadline in `titleLarge` at the top of the sheet. It is also the accessible name of the sheet when `accessibilityLabel` is not set.
headingLevelenum2NoThe outline level of the headline on web, where it renders as `<hN>`. Native has no heading levels and ignores it.
onBack() => void-NoShows a back button before the headline and calls this on press. Use it when the sheet shows a second level of content.
backIconIconSource'arrow-left' ('arrow-right' in RTL)NoIcon for the back button.
backAccessibilityLabelstring'Back'NoScreen-reader label for the back button.
closeIconIconSource'close'NoIcon for the close button. The close button shows when the sheet has an `onDismiss`.
closeAccessibilityLabelstring'Close'NoScreen-reader label for the close button.
actionsReactNode-NoAction 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.
showActionsDividerbooleantrueNoWhether a divider separates the actions from the content. Ignored without `actions`.
detachedbooleanfalseNoWhether the sheet is detached from the screen edges by a 16dp margin, with all four corners rounded.
widthnumber256NoWidth 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.
insetTopbooleantrue for 'modal', false for 'standard'NoWhether the sheet's content is padded by the top safe-area inset. The container color still extends under the inset.
insetBottombooleantrue for 'modal', false for 'standard'NoWhether the sheet's content is padded by the bottom safe-area inset.
containerColorstringsurfaceContainerLow for 'modal', surface for 'standard'NoOverride the sheet background.
contentColorstringonSurface for the headline, onSurfaceVariant for the iconsNoOverride the color of the headline and of the back and close icons. The hover and press layers of the two buttons derive from it.
headlineStyleStyleProp<TextStyle>-NoStyle applied to the headline `Text`.
styleStyleProp<ViewStyle>-NoStyle applied to the sheet surface.
accessibilityLabelstring-NoScreen-reader label for the sheet. Defaults to `headline`. A modal sheet is announced as a dialog, so it needs a name.
testIDstring-NoTest id applied to the sheet surface. A modal sheet's scrim carries `<testID>-scrim`, and the layer that slides carries `<testID>-layer`.
variantenum'modal'No-
visibleboolean-NoWhether the sheet is shown. Exit animations run before it unmounts.
onDismiss(() => void) | (() => void)-NoCalled 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.
dismissablebooleantrueNoWhether the scrim press, the Android back button, and Escape dismiss the sheet. The close button always calls `onDismiss`.
edgeenum'end'NoScreen edge the sheet enters from. `'end'` is the right edge in LTR and the left edge in RTL.
hostNamestring-NoRender into a specific named `PortalHost`.
scrimStyleany-NoStyle applied to the scrim.
scrimAccessibilityLabelstring'Close side sheet'NoScreen-reader label for the scrim's dismiss action.