This documentation is available as Markdown for AI agents and LLMs. See the full Markdown index or append .md to any documentation URL.

ModalBottomSheet

一个 Jetpack Compose ModalBottomSheet 组件,从屏幕底部渲染内容。

Android
Included in Expo Go
Recommended version:
~57.0.3

信息 有关跨平台使用,请参阅通用 BottomSheet —— 它会根据平台呈现相应的原生组件。

Expo UI ModalBottomSheet 与官方的 Jetpack Compose Bottom Sheet API 匹配,并在从底部滑出的模态面板中显示内容。

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

Modal bottom sheet with title, description, and action buttons

安装

🌐 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

使用 ref.hide() 在卸载前通过动画以编程方式关闭该面板。

🌐 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

当设置 skipPartiallyExpanded 时,表单会直接以完全展开的状态打开,而不是先停在半高位置。

🌐 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, height } 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), height(600)]}> <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> ); }

初始完全展开状态

🌐 Initial fully expanded state

initialFullyExpandedtrue 时,表单在首次组合时会直接以完全展开的状态打开,同时仍然可以访问部分状态。与 skipPartiallyExpanded 不同,用户仍然可以向下拖动以进入部分状态。partialExpand() 方法也继续有效。

🌐 When initialFullyExpanded is true, the sheet opens directly in the fully expanded state on first composition while leaving the partial state reachable. Unlike skipPartiallyExpanded, the user can still drag down to the partial state. The partialExpand() method also continues to work.

InitialFullyExpandedExample.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 InitialFullyExpandedExample() { 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)} initialFullyExpanded> <Column verticalArrangement={{ spacedBy: 12 }} modifiers={[paddingAll(24)]}> <Text>This sheet opened fully expanded.</Text> <Text>You can still drag it down to the partial state.</Text> <Button onClick={() => sheetRef.current?.partialExpand()}> <Text>Collapse to partial</Text> </Button> <Button onClick={hideSheet}> <Text>Close</Text> </Button> </Column> </ModalBottomSheet> )} </Host> ); }

自定义颜色

🌐 Custom colors

使用 containerColorcontentColorscrimColor 来自定义工作表的外观。

🌐 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

使用 ModalBottomSheet.DragHandle 插槽提供自定义拖动句柄,或设置 showDragHandle={false} 完全隐藏它。

🌐 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 内容

🌐 React Native content inside a bottom sheet

使用 RNHostView 在 Compose 底部弹出窗中嵌入交互式的 React Native 视图。这让你可以将 Compose 布局与 RN 组件如 PressableText 混合使用。

🌐 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> <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> ); }

带有 flex 的 React Native 内容

🌐 React Native content with flex

在不使用 matchContents 的情况下使用 RNHostView,让 RN 视图填充表单内部的剩余空间。与在父 Column 上使用固定的 height 修饰符结合,以控制表单的大小。

🌐 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> ); }

可滚动的 React Native 内容

🌐 Scrollable React Native content

在使用 RNHostView 的面板中嵌套一个可滚动的 React Native 列表,例如 FlatListScrollView,或者高性能列表如 FlashListLegend List。在可滚动组件上设置 nestedScrollEnabled,使其先滚动自身内容。一旦滚动到顶部边缘,剩余的拖动将移动面板。如果没有 nestedScrollEnabled,列表会消耗手势,而面板保持不动。

🌐 Nest a scrollable React Native list such as FlatList, ScrollView, or a high-performance list like FlashList or Legend List inside the sheet with RNHostView. Set nestedScrollEnabled on the scrollable so it scrolls its own content first. Once it reaches the top edge, the remaining drag moves the sheet. Without nestedScrollEnabled the list consumes the gesture and the sheet stays put.

ScrollableContentBottomSheetExample.tsx
import { useState } from 'react'; import { Host, ModalBottomSheet, Button, Column, RNHostView, Text } from '@expo/ui/jetpack-compose'; import { fillMaxHeight, padding } from '@expo/ui/jetpack-compose/modifiers'; import { FlatList, Text as RNText } from 'react-native'; const DATA = Array.from({ length: 50 }, (_, i) => `Item ${i + 1}`); export default function ScrollableContentBottomSheetExample() { const [visible, setVisible] = useState(false); return ( <Host matchContents> <Button onClick={() => setVisible(true)}> <Text>Open Scrollable Sheet</Text> </Button> {visible && ( <ModalBottomSheet onDismissRequest={() => setVisible(false)}> <Column modifiers={[fillMaxHeight(), padding(16, 16, 16, 16)]}> <RNHostView> <FlatList nestedScrollEnabled style={{ flex: 1 }} data={DATA} keyExtractor={item => item} renderItem={({ item }) => <RNText style={{ paddingVertical: 16 }}>{item}</RNText>} /> </RNHostView> </Column> </ModalBottomSheet> )} </Host> ); }

不可取消的面板

🌐 Non-dismissible sheet

propertiessheetGesturesEnabled 结合起来创建一个只能通过编程方式关闭的表单。

🌐 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

Type: ModalBottomSheetComponent

Props

children

Type: ReactNode

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

containerColor

Optional • Type: ColorValue

The background color of the bottom sheet.

contentColor

Optional • Type: ColorValue

The preferred color of the content inside the bottom sheet.

initialFullyExpanded

Only for:
Android

Optional • Type: boolean • Default: false

Opens the sheet fully expanded on first composition. Ignored when skipPartiallyExpanded is true.

modifiers

Optional • Type: ModifierConfig[]

Modifiers for the component.

onDismissRequest

Type: () => void

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

properties

Optional • Type: ModalBottomSheetProperties

Properties for the modal window behavior.

ref

Optional • Type: Ref<ModalBottomSheetRef>

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

scrimColor

Optional • Type: ColorValue

The color of the scrim overlay behind the bottom sheet.

sheetGesturesEnabled

Optional • Type: boolean • Default: true

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

showDragHandle

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

Optional • Type: boolean • Default: false

Immediately opens the bottom sheet in full screen.

Types

ModalBottomSheetProperties

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

PropertyTypeDescription
expand() => Promise<void>

Programmatically expands the bottom sheet to full height with an animation.

hide() => Promise<void>

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

partialExpand() => Promise<void>

Programmatically collapses the bottom sheet to partially expanded (~50%) state. Only works when skipPartiallyExpanded is false.