Reference version

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

BottomSheet

一个 SwiftUI 底部弹窗组件,从屏幕底部呈现内容。

iOS
tvOS
Included in Expo Go

Expo UI BottomSheet 与官方 SwiftUI sheet API 保持一致,并从屏幕底部展示内容。

一个处于中等停靠位置的 BottomSheet,显示一个排序列表,已选中“最近”一个处于中等停靠位置的 BottomSheet,显示一个排序列表,已选中“最近”

安装

Terminal
- npx expo install @expo/ui
- yarn expo install @expo/ui
- pnpm expo install @expo/ui
- bun expo install @expo/ui

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

使用

基本底部弹窗

将打开弹窗的控件作为 anchor 属性传入。子元素是弹窗的内容。

BasicBottomSheetExample.tsx
import { useState } from 'react'; import { Host, BottomSheet, Button, Text, VStack, } from '@expo/ui/swift-ui'; export default function BasicBottomSheetExample() { const [isPresented, setIsPresented] = useState(false); return ( <Host style={{ flex: 1 }}> <VStack> <BottomSheet isPresented={isPresented} onIsPresentedChange={setIsPresented} anchor={ <Button label="Open sheet" onPress={() => setIsPresented(true)} /> }> <Text>Hello, world!</Text> </BottomSheet> </VStack> </Host> ); }

将 React Native 视图用作锚点

锚点可以是 React Native 视图。将其包裹在 RNHostView 中。

BottomSheetRNAnchorExample.tsx
import { useState } from 'react'; import { Pressable, Text as RNText } from 'react-native'; import { Host, BottomSheet, Button, RNHostView, Text, VStack, } from '@expo/ui/swift-ui'; export default function BottomSheetRNAnchorExample() { const [isPresented, setIsPresented] = useState(false); return ( <Host style={{ flex: 1 }}> <VStack> <BottomSheet isPresented={isPresented} onIsPresentedChange={setIsPresented} fitToContents anchor={ <RNHostView matchContents> <Pressable onPress={() => setIsPresented(true)} style={{ backgroundColor: '#007AFF', padding: 12, borderRadius: 8, alignSelf: 'flex-start', }}> <RNText style={{ color: 'white', fontWeight: '600' }}> Open sheet </RNText> </Pressable> </RNHostView> }> <VStack> <Text>Opened from a React Native anchor.</Text> <Button label="Close" onPress={() => setIsPresented(false)} /> </VStack> </BottomSheet> </VStack> </Host> ); }

适配内容的底部弹窗

使用 fitToContents 属性可让弹窗自动调整大小以适配其内容。

BottomSheetFitsContentExample.tsx
import { useState } from 'react'; import { Host, BottomSheet, Button, Text, VStack, } from '@expo/ui/swift-ui'; export default function BottomSheetFitsContentExample() { const [isPresented, setIsPresented] = useState(false); return ( <Host style={{ flex: 1 }}> <VStack> <BottomSheet isPresented={isPresented} onIsPresentedChange={setIsPresented} fitToContents anchor={ <Button label="Open sheet" onPress={() => setIsPresented(true)} /> }> <VStack> <Text> This sheet automatically sizes to fit its content. </Text> <Button label="Close" onPress={() => setIsPresented(false)} /> </VStack> </BottomSheet> </VStack> </Host> ); }

自定义背景

默认情况下,弹窗使用系统的半透明材质背景(iOS 26 上为 Liquid Glass)。使用 Group 上的 presentationBackground 修饰符自行绘制弹窗背景,这也会使弹窗不再使用该半透明材质。它接受任何 ShapeStyle:纯色、材质或渐变。

BottomSheetBackgroundColorExample.tsx
import { useState } from 'react'; import { Host, BottomSheet, Button, Group, Text, VStack, } from '@expo/ui/swift-ui'; import { presentationBackground, presentationDetents, padding, foregroundStyle, } from '@expo/ui/swift-ui/modifiers'; export default function BottomSheetBackgroundColorExample() { const [isPresented, setIsPresented] = useState(false); return ( <Host style={{ flex: 1 }}> <VStack> <BottomSheet isPresented={isPresented} onIsPresentedChange={setIsPresented} anchor={ <Button label="Open sheet" onPress={() => setIsPresented(true)} /> }> <Group modifiers={[ presentationDetents(['medium', 'large']), presentationBackground('#ffffff'), ]}> <VStack modifiers={[padding({ all: 20 })]}> <Text modifiers={[foregroundStyle('#000000')]}> Solid white sheet background. </Text> <Button label="Close" onPress={() => setIsPresented(false)} /> </VStack> </Group> </BottomSheet> </VStack> </Host> ); }

带有停靠位置的底部弹窗

在 Group 上使用 presentationDetents 修饰器来控制可用高度。你可以使用:

  • 'medium':系统中等高度(大约半屏)
  • 'large':系统大高度(全屏)
  • { fraction: number }:屏幕高度的比例(0-1)
  • { height: number }:以点为单位的固定高度
BottomSheetWithDetentsExample.tsx
import { useState } from 'react'; import { Host, BottomSheet, Button, Group, Text, VStack, } from '@expo/ui/swift-ui'; import { presentationDetents } from '@expo/ui/swift-ui/modifiers'; export default function BottomSheetWithDetentsExample() { const [isPresented, setIsPresented] = useState(false); return ( <Host style={{ flex: 1 }}> <VStack> <BottomSheet isPresented={isPresented} onIsPresentedChange={setIsPresented} anchor={ <Button label="Open sheet" onPress={() => setIsPresented(true)} /> }> <Group modifiers={[ presentationDetents([ 'medium', 'large', { fraction: 0.3 }, { height: 200 }, ]), ]}> <Text>此弹窗可以停靠到多个高度。</Text> </Group> </BottomSheet> </VStack> </Host> ); }

跟踪停靠位置选择的底部弹窗

将 selection 和 onSelectionChange 选项传递给 presentationDetents,即可通过编程方式控制弹窗停靠到哪个停靠位置。

BottomSheetWithDetentSelectionExample.tsx
import { useState } from 'react'; import { Host, BottomSheet, Button, List, Section, Text, VStack, Group, } from '@expo/ui/swift-ui'; import { presentationDetents, presentationDragIndicator, foregroundStyle, } from '@expo/ui/swift-ui/modifiers'; import type { PresentationDetent } from '@expo/ui/swift-ui/modifiers'; export default function BottomSheetWithDetentSelectionExample() { const [isPresented, setIsPresented] = useState(false); const detents: PresentationDetent[] = [ { height: 300 }, { fraction: 0.3 }, 'medium', 'large', ]; const [selectedDetent, setSelectedDetent] = useState<PresentationDetent>('medium'); const formatDetent = (detent: PresentationDetent): string => { if (typeof detent === 'string') return detent; if ('fraction' in detent) return `比例 ${detent.fraction}`; return `高度 ${detent.height}`; }; return ( <Host style={{ flex: 1 }}> <VStack> <BottomSheet isPresented={isPresented} onIsPresentedChange={setIsPresented} anchor={ <Button label="Show sheet" onPress={() => setIsPresented(true)} /> }> <Group modifiers={[ presentationDetents(detents, { selection: selectedDetent, onSelectionChange: setSelectedDetent, }), presentationDragIndicator('visible'), ]}> <List> <Section title="Change detent"> <Button label="Height 300" onPress={() => setSelectedDetent({ height: 300 })} /> <Button label="Fraction 0.3" onPress={() => setSelectedDetent({ fraction: 0.3 }) } /> <Button label="Medium" onPress={() => setSelectedDetent('medium')} /> <Button label="Large" onPress={() => setSelectedDetent('large')} /> </Section> <Section title="Current"> <Text modifiers={[foregroundStyle('secondaryLabel')]}> {formatDetent(selectedDetent)} </Text> </Section> </List> </Group> </BottomSheet> </VStack> </Host> ); }

带背景交互的底部弹窗

使用 presentationBackgroundInteraction 修饰器,允许与弹窗后面的内容进行交互。

BottomSheetWithBackgroundInteractionExample.tsx
import { useState } from 'react'; import { Host, BottomSheet, Button, Group, Text, VStack, } from '@expo/ui/swift-ui'; import { presentationDetents, presentationBackgroundInteraction, } from '@expo/ui/swift-ui/modifiers'; export default function BottomSheetWithBackgroundInteractionExample() { const [isPresented, setIsPresented] = useState(false); return ( <Host style={{ flex: 1 }}> <VStack> <BottomSheet isPresented={isPresented} onIsPresentedChange={setIsPresented} anchor={ <Button label="Open sheet" onPress={() => setIsPresented(true)} /> }> <Group modifiers={[ presentationDetents(['medium', 'large']), presentationBackgroundInteraction({ type: 'enabledUpThrough', detent: 'medium', }), ]}> <Text> 处于中等高度时可与后面的内容交互。 </Text> </Group> </BottomSheet> </VStack> </Host> ); }

不可交互关闭的底部弹窗

使用 interactiveDismissDisabled 修饰器可防止用户通过滑动关闭弹窗。

NonDismissibleBottomSheetExample.tsx
import { useState } from 'react'; import { Host, BottomSheet, Button, Group, Text, VStack, } from '@expo/ui/swift-ui'; import { interactiveDismissDisabled } from '@expo/ui/swift-ui/modifiers'; export default function NonDismissibleBottomSheetExample() { const [isPresented, setIsPresented] = useState(false); return ( <Host style={{ flex: 1 }}> <VStack> <BottomSheet isPresented={isPresented} onIsPresentedChange={setIsPresented} anchor={ <Button label="Open sheet" onPress={() => setIsPresented(true)} /> }> <Group modifiers={[interactiveDismissDisabled()]}> <VStack> <Text> This sheet cannot be dismissed by swiping. </Text> <Button label="Close" onPress={() => setIsPresented(false)} /> </VStack> </Group> </BottomSheet> </VStack> </Host> ); }

带有 React Native 内容的底部弹窗

使用 RNHostView 将 React Native 组件嵌入到底部弹窗中。设置 matchContents 可让宿主视图自动调整大小以适配其内容。

BottomSheetWithRNContentExample.tsx
import { useState } from 'react'; import { PlatformColor, Pressable, Text as RNText, View, } from 'react-native'; import { Host, BottomSheet, Button, Group, RNHostView, VStack, } from '@expo/ui/swift-ui'; import { presentationDragIndicator } from '@expo/ui/swift-ui/modifiers'; export default function BottomSheetWithRNContentExample() { const [isPresented, setIsPresented] = useState(false); const [counter, setCounter] = useState(0); return ( <Host style={{ flex: 1 }}> <VStack> <BottomSheet isPresented={isPresented} onIsPresentedChange={setIsPresented} fitToContents anchor={ <Button label="Open sheet" onPress={() => setIsPresented(true)} /> }> <Group modifiers={[presentationDragIndicator('visible')]}> <RNHostView matchContents> <View style={{ padding: 24 }}> <RNText style={{ fontSize: 18, fontWeight: 'bold', marginBottom: 8, color: PlatformColor('label'), }}> React Native Content </RNText> <RNText style={{ color: '#666', marginBottom: 16 }}> Counter: {counter} </RNText> <Pressable style={{ backgroundColor: '#007AFF', padding: 12, borderRadius: 8, alignItems: 'center', marginBottom: 12, }} onPress={() => setCounter(counter + 1)}> <RNText style={{ color: 'white', fontWeight: '600' }}> Increment </RNText> </Pressable> <Pressable style={{ backgroundColor: '#FF3B30', padding: 12, borderRadius: 8, alignItems: 'center', }} onPress={() => setIsPresented(false)}> <RNText style={{ color: 'white', fontWeight: '600' }}> Close </RNText> </Pressable> </View> </RNHostView> </Group> </BottomSheet> </VStack> </Host> ); }

带有灵活 React Native 内容的底部弹窗

当使用带有 flex: 1 的 React Native 内容时,请省略 RNHostView 上的 matchContents 属性,并使用 presentationDetents 来控制弹窗高度。

BottomSheetWithFlexRNContentExample.tsx
import { useState } from 'react'; import { Text as RNText, View } from 'react-native'; import { Host, BottomSheet, Button, Group, RNHostView, VStack, } from '@expo/ui/swift-ui'; import { presentationDetents, presentationDragIndicator, } from '@expo/ui/swift-ui/modifiers'; export default function BottomSheetWithFlexRNContentExample() { const [isPresented, setIsPresented] = useState(false); return ( <Host style={{ flex: 1 }}> <VStack> <BottomSheet isPresented={isPresented} onIsPresentedChange={setIsPresented} anchor={ <Button label="Open sheet" onPress={() => setIsPresented(true)} /> }> <Group modifiers={[ presentationDetents(['medium', 'large']), presentationDragIndicator('visible'), ]}> <RNHostView> <View style={{ flex: 1, backgroundColor: '#007AFF', padding: 24, }}> <RNText style={{ fontSize: 18, fontWeight: 'bold', color: 'white', }}> Flexible React Native Content </RNText> <RNText style={{ color: 'white', marginTop: 8 }}> This content fills the available space in the sheet. </RNText> </View> </RNHostView> </Group> </BottomSheet> </VStack> </Host> ); }

可滚动的 React Native 内容

将可滚动的 React Native 列表,例如 FlatList、ScrollView,或高性能列表(如 FlashList 或 Legend List)嵌套在带有 RNHostView 的弹窗中。使用 presentationDetents 设置弹窗尺寸,列表会在该高度内滚动。

BottomSheetWithScrollableContentExample.tsx
import { useState } from 'react'; import { FlatList, PlatformColor, Text as RNText, View, } from 'react-native'; import { Host, BottomSheet, Button, Group, RNHostView, VStack, } from '@expo/ui/swift-ui'; import { presentationDetents, presentationDragIndicator, } from '@expo/ui/swift-ui/modifiers'; const DATA = Array.from({ length: 50 }, (_, i) => `项目 ${i + 1}`); export default function BottomSheetWithScrollableContentExample() { const [isPresented, setIsPresented] = useState(false); return ( <Host style={{ flex: 1 }}> <VStack> <BottomSheet isPresented={isPresented} onIsPresentedChange={setIsPresented} anchor={ <Button label="打开弹出面板" onPress={() => setIsPresented(true)} /> }> <Group modifiers={[ presentationDetents(['medium', 'large']), presentationDragIndicator('visible'), ]}> <RNHostView> <View style={{ padding: 16 }}> <FlatList data={DATA} keyExtractor={item => item} renderItem={({ item }) => ( <RNText style={{ paddingVertical: 16, color: PlatformColor('label'), }}> {item} </RNText> )} /> </View> </RNHostView> </Group> </BottomSheet> </VStack> </Host> ); }

API

import { BottomSheet } from '@expo/ui/swift-ui';

Component

BottomSheet

iOS
tvOS

Type: React.Element<BottomSheetProps>

BottomSheet presents content from the bottom of the screen.

BottomSheetProps

anchor

iOS
tvOS
Optional • Type: ReactNode

A view the sheet is anchored to, for example the Button that opens it. Rendered in place and kept mounted, so presenting the sheet doesn't shift surrounding layout. Optional.

children

iOS
tvOS
Type: ReactNode

The sheet's content, mounted while presented and unmounted after dismiss. Wrap it in Group to apply presentation modifiers.

fitToContents

iOS
tvOS
Optional • Type: boolean • Default: false

When true, the sheet will automatically size itself to fit its content. This sets the presentation detent to match the height of the children.

isPresented

iOS
tvOS
Type: boolean

Whether the BottomSheet is presented.

onDismiss

iOS
tvOS
Optional • Type: () => void

Callback function that is called after the BottomSheet has been fully dismissed.

onIsPresentedChange

iOS
tvOS
Type: (isPresented: boolean) => void

Callback function that is called when the BottomSheet presented state changes.