Reference version

This is documentation for the next SDK version. For up-to-date documentation, see the latest version (SDK 55).

ModalBottomSheet

A Jetpack Compose ModalBottomSheet component that presents content from the bottom of the screen.

Android

Expo UI ModalBottomSheet matches the official Jetpack Compose Bottom Sheet API and displays content in a modal sheet that slides up from the bottom.

Installation

Terminal
npx expo install @expo/ui

If you are installing this in an existing React Native app, make sure to install expo in your project.

Usage

Basic bottom sheet

Use ref.hide() to programmatically dismiss the sheet with an animation before unmounting it.

BasicBottomSheetExample.tsx
import { useRef, useState } from 'react'; import { Host, ModalBottomSheet, Button, Column, Text } from '@expo/ui/jetpack-compose'; import type { ModalBottomSheetRef } from '@expo/ui/jetpack-compose'; import { paddingAll } from '@expo/ui/jetpack-compose/modifiers'; export default function BasicBottomSheetExample() { const [visible, setVisible] = useState(false); const sheetRef = useRef<ModalBottomSheetRef>(null); const hideSheet = async () => { await sheetRef.current?.hide(); setVisible(false); }; return ( <Host matchContents> <Button onClick={() => setVisible(true)}> <Text>Open Sheet</Text> </Button> {visible && ( <ModalBottomSheet ref={sheetRef} onDismissRequest={() => setVisible(false)}> <Column verticalArrangement={{ spacedBy: 12 }} modifiers={[paddingAll(24)]}> <Text>Hello from bottom sheet!</Text> <Text>You can add more content here.</Text> <Button onClick={hideSheet}> <Text>Close</Text> </Button> </Column> </ModalBottomSheet> )} </Host> ); }

Skip partially expanded state

When skipPartiallyExpanded is set, the sheet opens directly in the fully expanded state instead of stopping at the half-height position first.

SkipPartiallyExpandedExample.tsx
import { useRef, useState } from 'react'; import { Host, ModalBottomSheet, Button, Column, Text } from '@expo/ui/jetpack-compose'; import type { ModalBottomSheetRef } from '@expo/ui/jetpack-compose'; import { paddingAll } from '@expo/ui/jetpack-compose/modifiers'; export default function SkipPartiallyExpandedExample() { const [visible, setVisible] = useState(false); const sheetRef = useRef<ModalBottomSheetRef>(null); const hideSheet = async () => { await sheetRef.current?.hide(); setVisible(false); }; return ( <Host matchContents> <Button onClick={() => setVisible(true)}> <Text>Open Sheet</Text> </Button> {visible && ( <ModalBottomSheet ref={sheetRef} onDismissRequest={() => setVisible(false)} skipPartiallyExpanded> <Column verticalArrangement={{ spacedBy: 12 }} modifiers={[paddingAll(24)]}> <Text>This sheet skips the partially expanded state.</Text> <Text>It opens directly in the fully expanded position.</Text> <Button onClick={hideSheet}> <Text>Close</Text> </Button> </Column> </ModalBottomSheet> )} </Host> ); }

Custom colors

Use containerColor, contentColor, and scrimColor to customize the sheet's appearance.

CustomColorsExample.tsx
import { useRef, useState } from 'react'; import { Host, ModalBottomSheet, Button, Column, Text } from '@expo/ui/jetpack-compose'; import type { ModalBottomSheetRef } from '@expo/ui/jetpack-compose'; import { paddingAll } from '@expo/ui/jetpack-compose/modifiers'; export default function CustomColorsExample() { const [visible, setVisible] = useState(false); const sheetRef = useRef<ModalBottomSheetRef>(null); const hideSheet = async () => { await sheetRef.current?.hide(); setVisible(false); }; return ( <Host matchContents> <Button onClick={() => setVisible(true)}> <Text>Open Colored Sheet</Text> </Button> {visible && ( <ModalBottomSheet ref={sheetRef} onDismissRequest={() => setVisible(false)} containerColor="#1a1a2e" contentColor="#e0e0e0" scrimColor="#806200EE"> <Column verticalArrangement={{ spacedBy: 12 }} modifiers={[paddingAll(24)]}> <Text>Custom styled bottom sheet.</Text> <Text>Dark container with a purple scrim overlay.</Text> <Button onClick={hideSheet}> <Text>Close</Text> </Button> </Column> </ModalBottomSheet> )} </Host> ); }

Custom drag handle

Use ModalBottomSheet.DragHandle slot to provide a custom drag handle, or set showDragHandle={false} to hide it entirely.

CustomDragHandleExample.tsx
import { useRef, useState } from 'react'; import { Host, ModalBottomSheet, Button, Column, Box, Text } from '@expo/ui/jetpack-compose'; import type { ModalBottomSheetRef } from '@expo/ui/jetpack-compose'; import { background, clip, fillMaxWidth, height, padding, Shapes, width, } from '@expo/ui/jetpack-compose/modifiers'; export default function CustomDragHandleExample() { const [visible, setVisible] = useState(false); const sheetRef = useRef<ModalBottomSheetRef>(null); const hideSheet = async () => { await sheetRef.current?.hide(); setVisible(false); }; return ( <Host matchContents> <Button onClick={() => setVisible(true)}> <Text>Open Custom Handle Sheet</Text> </Button> {visible && ( <ModalBottomSheet ref={sheetRef} onDismissRequest={() => setVisible(false)}> <ModalBottomSheet.DragHandle> <Column horizontalAlignment="center" modifiers={[fillMaxWidth(), padding(0, 12, 0, 8)]}> <Box modifiers={[width(60), height(6), clip(Shapes.Circle), background('#6200EE')]} /> </Column> </ModalBottomSheet.DragHandle> <Column verticalArrangement={{ spacedBy: 12 }} modifiers={[padding(16, 16, 16, 16)]}> <Button onClick={hideSheet}> <Text>Close</Text> </Button> </Column> </ModalBottomSheet> )} </Host> ); }

React Native content inside a bottom sheet

Use RNHostView to embed interactive React Native views inside a Compose bottom sheet. This lets you mix Compose layout with RN components like Pressable and Text.

RNContentBottomSheetExample.tsx
import { useRef, useState } from 'react'; import { Host, ModalBottomSheet, Button, Column, RNHostView, Text } from '@expo/ui/jetpack-compose'; import type { ModalBottomSheetRef } from '@expo/ui/jetpack-compose'; import { padding } from '@expo/ui/jetpack-compose/modifiers'; import { Pressable, Text as RNText, View } from 'react-native'; export default function RNContentBottomSheetExample() { const [visible, setVisible] = useState(false); const sheetRef = useRef<ModalBottomSheetRef>(null); const hideSheet = async () => { await sheetRef.current?.hide(); setVisible(false); }; return ( <Host matchContents> <Button onClick={() => setVisible(true)}> <Text>Open RN Content Sheet</Text> </Button> {visible && ( <ModalBottomSheet ref={sheetRef} onDismissRequest={() => setVisible(false)} skipPartiallyExpanded={false}> <Column verticalArrangement={{ spacedBy: 16 }} modifiers={[padding(16, 16, 16, 16)]}> <Text>Mixing Compose + RN in a Bottom Sheet</Text> <RNHostView matchContents> <View> <RNText style={{ fontSize: 18, fontWeight: 'bold', marginBottom: 8 }}> React Native Content </RNText> <Pressable style={{ backgroundColor: '#007AFF', padding: 12, borderRadius: 8, alignItems: 'center', }} onPress={hideSheet}> <RNText style={{ color: 'white', fontWeight: '600' }}>Close</RNText> </Pressable> </View> </RNHostView> </Column> </ModalBottomSheet> )} </Host> ); }

React Native content with flex

Use RNHostView without matchContents to let the RN view fill the remaining space inside the sheet. Combine with a fixed height modifier on the parent Column to control the sheet size.

FlexRNContentExample.tsx
import { useRef, useState } from 'react'; import { Host, ModalBottomSheet, Button, Column, RNHostView, Text } from '@expo/ui/jetpack-compose'; import type { ModalBottomSheetRef } from '@expo/ui/jetpack-compose'; import { height, padding } from '@expo/ui/jetpack-compose/modifiers'; import { Text as RNText, View } from 'react-native'; export default function FlexRNContentExample() { const [visible, setVisible] = useState(false); const sheetRef = useRef<ModalBottomSheetRef>(null); const hideSheet = async () => { await sheetRef.current?.hide(); setVisible(false); }; return ( <Host matchContents> <Button onClick={() => setVisible(true)}> <Text>Open Flex Content Sheet</Text> </Button> {visible && ( <ModalBottomSheet ref={sheetRef} onDismissRequest={() => setVisible(false)} skipPartiallyExpanded> <Column modifiers={[height(400), padding(16, 16, 16, 16)]}> <Text>RN View with flex: 1</Text> <RNHostView> <View style={{ flex: 1, backgroundColor: '#9B59B6', borderRadius: 10 }}> <RNText style={{ color: 'white', fontSize: 18, fontWeight: 'bold', padding: 16, }}> React Native Content (flex: 1) </RNText> </View> </RNHostView> </Column> </ModalBottomSheet> )} </Host> ); }

Non-dismissible sheet

Combine properties, sheetGesturesEnabled to create a sheet that can only be closed programmatically.

NonDismissibleExample.tsx
import { useRef, useState } from 'react'; import { Host, ModalBottomSheet, Button, Column, Text } from '@expo/ui/jetpack-compose'; import type { ModalBottomSheetRef } from '@expo/ui/jetpack-compose'; import { paddingAll } from '@expo/ui/jetpack-compose/modifiers'; export default function NonDismissibleExample() { const [visible, setVisible] = useState(false); const sheetRef = useRef<ModalBottomSheetRef>(null); const hideSheet = async () => { await sheetRef.current?.hide(); setVisible(false); }; return ( <Host matchContents> <Button onClick={() => setVisible(true)}> <Text>Open Non-Dismissible Sheet</Text> </Button> {visible && ( <ModalBottomSheet ref={sheetRef} onDismissRequest={() => setVisible(false)} sheetGesturesEnabled={false} properties={{ shouldDismissOnBackPress: false, shouldDismissOnClickOutside: false, }}> <Column verticalArrangement={{ spacedBy: 12 }} modifiers={[paddingAll(24)]}> <Text>This sheet cannot be dismissed by swiping, back press, or tapping outside.</Text> <Text>Only the button below will close it.</Text> <Button onClick={hideSheet}> <Text>Close</Text> </Button> </Column> </ModalBottomSheet> )} </Host> ); }

API

import { ModalBottomSheet } from '@expo/ui/jetpack-compose';

Constants

BottomSheet.ModalBottomSheet

Android

Type: ModalBottomSheetComponent

Props

children

Android
Type: React.ReactNode

The children of the ModalBottomSheet component. Can include a ModalBottomSheet.DragHandle slot for a custom drag handle.

containerColor

Android
Optional • Type: ColorValue

The background color of the bottom sheet.

contentColor

Android
Optional • Type: ColorValue

The preferred color of the content inside the bottom sheet.

modifiers

Android
Optional • Type: ModifierConfig[]

Modifiers for the component.

onDismissRequest

Android
Type: () => void

Callback function that is called when the user dismisses the bottom sheet (via swipe, back press, or tapping outside the scrim).

properties

Android
Optional • Type: ModalBottomSheetProperties

Properties for the modal window behavior.

ref

Android
Optional • Type: Ref<ModalBottomSheetRef>

Can be used to imperatively hide the bottom sheet with an animation.

scrimColor

Android
Optional • Type: ColorValue

The color of the scrim overlay behind the bottom sheet.

sheetGesturesEnabled

Android
Optional • Type: boolean • Default: true

Whether gestures (swipe to dismiss) are enabled on the bottom sheet.

showDragHandle

Android
Optional • Type: boolean • Default: true

Whether to show the default drag handle at the top of the bottom sheet. Ignored if a custom ModalBottomSheet.DragHandle slot is provided.

skipPartiallyExpanded

Android
Optional • Type: boolean • Default: false

Immediately opens the bottom sheet in full screen.

Types

ModalBottomSheetProperties

Android
PropertyTypeDescription
shouldDismissOnBackPress(optional)boolean

Whether the bottom sheet can be dismissed by pressing the back button.

Default:true
shouldDismissOnClickOutside(optional)boolean

Whether the bottom sheet can be dismissed by clicking outside (on the scrim).

Default:true

ModalBottomSheetRef

Android
PropertyTypeDescription
hide() => Promise<void>

Programmatically hides the bottom sheet with an animation. The returned promise resolves after the dismiss animation completes.