truesheet-usage

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

TrueSheet Consumer Guide

TrueSheet用户指南

Use this skill to produce correct, idiomatic code for apps that consume
@lodev09/react-native-true-sheet
. It covers choosing the right integration pattern, applying the public API correctly, and avoiding platform-specific pitfalls.
本指南用于为使用
@lodev09/react-native-true-sheet
的应用生成正确、符合规范的代码,涵盖选择合适的集成模式、正确使用公共API以及规避各平台的常见踩坑点。

Quick Start

快速开始

The simplest sheet: a ref, a button, and some content.
tsx
import { useRef } from 'react'
import { Button, Text, View } from 'react-native'
import { TrueSheet } from '@lodev09/react-native-true-sheet'

export function App() {
  const sheet = useRef<TrueSheet>(null)

  return (
    <View>
      <Button title="Open" onPress={() => sheet.current?.present()} />
      <TrueSheet ref={sheet} detents={['auto']} cornerRadius={24} grabber>
        <View style={{ padding: 16 }}>
          <Text>Hello from the sheet</Text>
          <Button title="Close" onPress={() => sheet.current?.dismiss()} />
        </View>
      </TrueSheet>
    </View>
  )
}
最简单的弹窗实现:一个ref、一个按钮加少量内容。
tsx
import { useRef } from 'react'
import { Button, Text, View } from 'react-native'
import { TrueSheet } from '@lodev09/react-native-true-sheet'

export function App() {
  const sheet = useRef<TrueSheet>(null)

  return (
    <View>
      <Button title="打开" onPress={() => sheet.current?.present()} />
      <TrueSheet ref={sheet} detents={['auto']} cornerRadius={24} grabber>
        <View style={{ padding: 16 }}>
          <Text>来自弹窗的问候</Text>
          <Button title="关闭" onPress={() => sheet.current?.dismiss()} />
        </View>
      </TrueSheet>
    </View>
  )
}

Choose the Right Control Pattern

选择合适的控制模式

Pick one based on where the trigger lives relative to the sheet and which platforms you target.
PatternWhen to usePlatform
RefTrigger and sheet in the same componentAll
Named + global methodsTrigger is far from the sheet (different screen, deep in tree)Native only
TrueSheetProvider
+
useTrueSheet()
Web support needed, or you want hook-based controlAll (required on web)
createTrueSheetNavigator()
Sheets are part of a navigation flowAll
ReanimatedTrueSheet
You need animated values synced to sheet positionAll
根据触发按钮和弹窗的相对位置、以及你要支持的平台选择对应的模式:
模式适用场景支持平台
Ref触发按钮和弹窗在同一个组件内全平台
命名+全局方法触发按钮和弹窗距离较远(不同页面、组件树深层)仅原生端
TrueSheetProvider
+
useTrueSheet()
需要支持web端,或是想要基于hook的控制方式全平台(web端必填)
createTrueSheetNavigator()
弹窗属于导航流程的一部分全平台
ReanimatedTrueSheet
需要让动画值和弹窗位置同步全平台

Ref-based

基于Ref的模式

Already shown in Quick Start. Use
present()
,
dismiss()
,
resize(index)
on the ref.
已经在快速开始部分演示过,直接在ref上调用
present()
dismiss()
resize(index)
即可。

Named sheet with global methods (native only)

带全局方法的命名弹窗(仅原生端)

When the trigger is far from where the sheet renders:
tsx
// Somewhere in the tree
<TrueSheet name="profile" detents={['auto', 1]}>
  <ProfileContent />
</TrueSheet>

// Anywhere else (native only)
await TrueSheet.present('profile')
await TrueSheet.dismiss('profile')
await TrueSheet.resize('profile', 1)
await TrueSheet.dismissAll()
Every
name
must be unique. Static methods don't exist on web — use the provider pattern instead.
当触发按钮和弹窗渲染位置距离较远时使用:
tsx
// 组件树的任意位置
<TrueSheet name="profile" detents={['auto', 1]}>
  <ProfileContent />
</TrueSheet>

// 其他任意位置(仅原生端)
await TrueSheet.present('profile')
await TrueSheet.dismiss('profile')
await TrueSheet.resize('profile', 1)
await TrueSheet.dismissAll()
每个
name
必须唯一。静态方法在web端不存在——请改用Provider模式。

Web control with provider

用Provider实现Web端控制

Wrap your app with
TrueSheetProvider
(on native this is a pass-through with zero overhead):
tsx
import { TrueSheet, TrueSheetProvider, useTrueSheet } from '@lodev09/react-native-true-sheet'

function Toolbar() {
  const { present, dismiss } = useTrueSheet()
  return <Button title="Open" onPress={() => present('settings')} />
}

export function App() {
  return (
    <TrueSheetProvider>
      <Toolbar />
      <TrueSheet name="settings" detents={[0.5, 1]}>
        <SettingsContent />
      </TrueSheet>
    </TrueSheetProvider>
  )
}
TrueSheetProvider
包裹你的应用(原生端这个组件是无开销的透传组件):
tsx
import { TrueSheet, TrueSheetProvider, useTrueSheet } from '@lodev09/react-native-true-sheet'

function Toolbar() {
  const { present, dismiss } = useTrueSheet()
  return <Button title="打开" onPress={() => present('settings')} />
}

export function App() {
  return (
    <TrueSheetProvider>
      <Toolbar />
      <TrueSheet name="settings" detents={[0.5, 1]}>
        <SettingsContent />
      </TrueSheet>
    </TrueSheetProvider>
  )
}

Navigation (React Navigation / Expo Router)

导航(React Navigation / Expo Router)

See advanced patterns reference for full setup with
createTrueSheetNavigator
, Expo Router layouts, screen options, and
useTrueSheetNavigation
.
查看高级模式参考了解
createTrueSheetNavigator
、Expo Router布局、页面配置、
useTrueSheetNavigation
的完整配置方式。

Reanimated

Reanimated

See advanced patterns reference for
ReanimatedTrueSheet
,
ReanimatedTrueSheetProvider
, and animated values (
animatedPosition
,
animatedIndex
,
animatedDetent
).
查看高级模式参考了解
ReanimatedTrueSheet
ReanimatedTrueSheetProvider
以及动画值(
animatedPosition
animatedIndex
animatedDetent
)的用法。

Detents

锚点(Detents)

Detents define the heights the sheet can snap to. You get up to 3 detents, sorted smallest to largest.
ValueMeaning
'auto'
Size to fit the content (iOS 16+, Android, Web)
0
1
Fraction of the screen height
tsx
// Content-sized sheet
<TrueSheet detents={['auto']} />

// Half and full screen
<TrueSheet detents={[0.5, 1]} />

// Three stops: peek, half, full
<TrueSheet detents={[0.25, 0.5, 1]} />
The one rule you can't break: never combine
'auto'
with
scrollable
. Auto-sizing needs to measure the full content, but a scrollable sheet clips it — they're fundamentally incompatible. Use fractional detents for scrollable sheets.
锚点定义了弹窗可以停靠的高度,最多支持3个锚点,需要按从小到大排序。
取值含义
'auto'
大小适配内容高度(iOS 16+、Android、Web)
0
1
屏幕高度的占比
tsx
// 适配内容高度的弹窗
<TrueSheet detents={['auto']} />

// 半屏和全屏
<TrueSheet detents={[0.5, 1]} />

// 三个停靠位置:预览、半屏、全屏
<TrueSheet detents={[0.25, 0.5, 1]} />
不可违反的规则: 永远不要同时使用
'auto'
scrollable
。自动高度需要测量完整内容高度,但可滚动弹窗会裁剪内容——二者本质上不兼容。可滚动弹窗请使用占比类型的锚点。

Common Recipes

常见实现方案

Scrollable content

可滚动内容

tsx
<TrueSheet detents={[0.5, 1]} scrollable cornerRadius={24} grabber>
  <ScrollView>
    {items.map(item => <ItemRow key={item.id} item={item} />)}
  </ScrollView>
</TrueSheet>
  • The
    scrollable
    prop auto-detects ScrollView/FlatList up to 2 levels deep
  • On iOS, scrolling to top expands to next detent — disable with
    scrollableOptions={{ scrollingExpandsSheet: false }}
  • On Android, nested scrolling is handled automatically
tsx
<TrueSheet detents={[0.5, 1]} scrollable cornerRadius={24} grabber>
  <ScrollView>
    {items.map(item => <ItemRow key={item.id} item={item} />)}
  </ScrollView>
</TrueSheet>
  • scrollable
    属性会自动检测最深2层内的ScrollView/FlatList
  • iOS上滚动到顶部会自动展开到下一个锚点——可以通过
    scrollableOptions={{ scrollingExpandsSheet: false }}
    关闭该特性
  • Android上的嵌套滚动会自动处理

Fixed header and footer

固定头部和底部

tsx
<TrueSheet
  detents={[0.5, 1]}
  scrollable
  header={
    <View style={{ padding: 16 }}>
      <Text style={{ fontSize: 18, fontWeight: 'bold' }}>Title</Text>
    </View>
  }
  footer={<BottomActions />}
>
  <ScrollView>{/* ... */}</ScrollView>
</TrueSheet>
Use the
header
and
footer
props — they render in native container views, so the layout math is handled for you. Don't fake it with absolute positioning.
tsx
<TrueSheet
  detents={[0.5, 1]}
  scrollable
  header={
    <View style={{ padding: 16 }}>
      <Text style={{ fontSize: 18, fontWeight: 'bold' }}>标题</Text>
    </View>
  }
  footer={<BottomActions />}
>
  <ScrollView>{/* ... */}</ScrollView>
</TrueSheet>
请使用
header
footer
属性——它们会渲染在原生容器视图中,布局计算会自动处理。不要用绝对定位自己实现。

Non-dismissible confirmation

不可关闭的确认弹窗

tsx
<TrueSheet
  ref={sheet}
  detents={['auto']}
  dismissible={false}
  draggable={false}
  dimmed
  grabber={false}
>
  <View style={{ padding: 24 }}>
    <Text>Are you sure?</Text>
    <Button title="Confirm" onPress={handleConfirm} />
    <Button title="Cancel" onPress={() => sheet.current?.dismiss()} />
  </View>
</TrueSheet>
tsx
<TrueSheet
  ref={sheet}
  detents={['auto']}
  dismissible={false}
  draggable={false}
  dimmed
  grabber={false}
>
  <View style={{ padding: 24 }}>
    <Text>你确定吗?</Text>
    <Button title="确认" onPress={handleConfirm} />
    <Button title="取消" onPress={() => sheet.current?.dismiss()} />
  </View>
</TrueSheet>

iOS blur background

iOS模糊背景

tsx
<TrueSheet detents={['auto']} backgroundBlur="system-material">
  <View style={{ padding: 16 }}>
    <Text>Blurred sheet</Text>
  </View>
</TrueSheet>
Fine-tune with
blurOptions={{ intensity: 80, interaction: true }}
. Blur is iOS-only.
tsx
<TrueSheet detents={['auto']} backgroundBlur="system-material">
  <View style={{ padding: 16 }}>
    <Text>模糊背景弹窗</Text>
  </View>
</TrueSheet>
可以通过
blurOptions={{ intensity: 80, interaction: true }}
微调效果。模糊效果仅iOS支持。

Present on mount

组件挂载时自动展示

tsx
<TrueSheet detents={['auto', 1]} initialDetentIndex={0} initialDetentAnimated>
  <WelcomeContent />
</TrueSheet>
tsx
<TrueSheet detents={['auto', 1]} initialDetentIndex={0} initialDetentAnimated>
  <WelcomeContent />
</TrueSheet>

Dimming control

遮罩控制

tsx
// No dimming (allows background interaction)
<TrueSheet dimmed={false} detents={['auto']} />

// Dim only above a certain detent
<TrueSheet detents={['auto', 0.7, 1]} dimmedDetentIndex={1} />
tsx
// 无遮罩(允许和背景交互)
<TrueSheet dimmed={false} detents={['auto']} />

// 仅超过指定锚点时显示遮罩
<TrueSheet detents={['auto', 0.7, 1]} dimmedDetentIndex={1} />

Resize programmatically

代码控制尺寸调整

resize()
takes a detent index, not a value:
tsx
const sheet = useRef<TrueSheet>(null)

// detents={[0.3, 0.6, 1]}
await sheet.current?.resize(2) // expands to full (index 2)
resize()
接收的是锚点索引,不是锚点值:
tsx
const sheet = useRef<TrueSheet>(null)

// detents={[0.3, 0.6, 1]}
await sheet.current?.resize(2) // 展开到全屏(索引2)

Rules That Save Debugging Time

节省调试时间的规则

  1. Max 3 detents, sorted smallest → largest.
  2. Never
    'auto'
    +
    scrollable
    — they're incompatible.
  3. resize()
    takes an index
    , not a fraction.
    resize(1)
    means "go to the second detent."
  4. Sheet names must be unique across your entire app.
  5. Static methods are native-only — use
    useTrueSheet()
    on web.
  6. Don't use
    autoFocus
    on TextInputs
    inside sheets. Focus in
    onDidPresent
    instead:
    tsx
    <TrueSheet onDidPresent={() => inputRef.current?.focus()}>
  7. Use
    flexGrow: 1
    (not
    flex: 1
    ) inside
    GestureHandlerRootView
    on Android.
  8. Dismiss sheets before closing Modals on iOS — React Native has a bug where dismissing a Modal while a sheet is visible causes a blank screen.
  9. Use
    header
    /
    footer
    props
    for fixed chrome — don't reach for absolute positioning.
  10. Liquid Glass is automatic on iOS 26+. Set
    backgroundColor
    to disable it per-sheet, or add
    UIDesignRequiresCompatibility
    to Info.plist to disable app-wide.
  1. 最多3个锚点,按从小到大排序。
  2. 永远不要同时用
    'auto'
    scrollable
    ——二者不兼容。
  3. resize()
    接收的是索引
    ,不是占比值。
    resize(1)
    表示「跳转到第二个锚点」。
  4. 弹窗名称在整个应用内必须唯一
  5. 静态方法仅原生端支持——web端请使用
    useTrueSheet()
  6. 不要在弹窗内的TextInputs上用
    autoFocus
    。请在
    onDidPresent
    中处理聚焦:
    tsx
    <TrueSheet onDidPresent={() => inputRef.current?.focus()}>
  7. Android平台的
    GestureHandlerRootView
    内部请使用
    flexGrow: 1
    (不要用
    flex: 1
    )。
  8. iOS平台关闭Modal前请先关闭弹窗——React Native存在bug,当弹窗可见时关闭Modal会导致白屏。
  9. 固定元素请用
    header
    /
    footer
    属性
    ——不要用绝对定位实现。
  10. 液态玻璃效果在iOS 26+上自动开启。设置
    backgroundColor
    可以单弹窗禁用,或是在Info.plist中添加
    UIDesignRequiresCompatibility
    全局禁用。

Platform Differences at a Glance

平台差异速览

FeatureiOSAndroidWeb
'auto'
detent
iOS 16+YesYes
backgroundBlur
YesNoNo
Liquid GlassiOS 26+NoNo
Static global methodsYesYesNo (use provider)
scrollable
YesYesNo (use
BottomSheetScrollView
)
anchor
/ side sheets
System-controlled margins
anchorOffset
prop
anchorOffset
prop
pageSizing
(iPad)
iOS 17+N/AN/A
detached
mode
NoNoYes
stackBehavior
N/AN/A
push
/
switch
/
replace
Edge-to-edgeN/AAuto-detectedN/A
Keyboard handlingBuilt-inBuilt-inN/A
功能iOSAndroidWeb
'auto'
锚点
iOS 16+支持支持
backgroundBlur
支持不支持不支持
液态玻璃效果iOS 26+不支持不支持
静态全局方法支持支持不支持(用Provider)
scrollable
支持支持不支持(用
BottomSheetScrollView
anchor
/侧边弹窗
系统控制边距支持
anchorOffset
属性
支持
anchorOffset
属性
pageSizing
(iPad)
iOS 17+不适用不适用
detached
模式
不支持不支持支持
stackBehavior
不适用不适用
push
/
switch
/
replace
边到边显示不适用自动检测不适用
键盘处理内置内置不适用

Events

事件

The most commonly used events:
EventWhen it firesPayload
onMount
Content is mounted and ready
onDidPresent
Sheet finished presenting
{ index, position, detent }
onDidDismiss
Sheet finished dismissing
onDetentChange
User dragged or
resize()
changed the detent
{ index, position, detent }
onPositionChange
Continuous position updates during drag/animation
{ index, position, detent, realtime }
For the full event list (drag events, focus/blur events, will/did lifecycle pairs,
onBackPress
), see the API reference.
最常用的事件:
事件触发时机携带参数
onMount
内容挂载完成就绪
onDidPresent
弹窗完全展示完成
{ index, position, detent }
onDidDismiss
弹窗完全关闭完成
onDetentChange
用户拖动或
resize()
触发锚点切换
{ index, position, detent }
onPositionChange
拖动/动画过程中持续触发位置更新
{ index, position, detent, realtime }
完整事件列表(拖动事件、聚焦/失焦事件、will/did生命周期对、
onBackPress
)请查看API参考

Methods

方法

On a ref:
  • present(index?, animated?)
    — show the sheet
  • dismiss(animated?)
    — hide the sheet and all its children
  • dismissStack(animated?)
    — hide only sheets stacked on top
  • resize(index)
    — snap to a detent by index
Global (native only):
  • TrueSheet.present(name, index?, animated?)
  • TrueSheet.dismiss(name, animated?)
  • TrueSheet.dismissStack(name, animated?)
  • TrueSheet.resize(name, index)
  • TrueSheet.dismissAll(animated?)
Web hook:
tsx
const { present, dismiss, dismissStack, resize, dismissAll } = useTrueSheet()
Ref上的方法:
  • present(index?, animated?)
    — 展示弹窗
  • dismiss(animated?)
    — 隐藏弹窗及其所有子弹窗
  • dismissStack(animated?)
    — 仅隐藏堆叠在当前弹窗之上的弹窗
  • resize(index)
    — 按索引跳转到指定锚点
全局方法(仅原生端):
  • TrueSheet.present(name, index?, animated?)
  • TrueSheet.dismiss(name, animated?)
  • TrueSheet.dismissStack(name, animated?)
  • TrueSheet.resize(name, index)
  • TrueSheet.dismissAll(animated?)
Web端Hook:
tsx
const { present, dismiss, dismissStack, resize, dismissAll } = useTrueSheet()

Stacking Sheets

弹窗堆叠

Present a new sheet while another is visible and the first one hides automatically. Dismiss the top sheet and the previous one comes back. This is built-in — no extra config needed.
  • dismiss()
    cascades: it dismisses the current sheet plus everything stacked on top
  • dismissStack()
    dismisses only the sheets on top, keeping the current one visible
  • Use
    onDidFocus
    /
    onDidBlur
    to react to a sheet gaining or losing the top position
在已有弹窗可见的情况下展示新弹窗,旧弹窗会自动隐藏。关闭最上层弹窗后,下层弹窗会自动恢复。该特性是内置的——无需额外配置。
  • dismiss()
    会级联关闭:会关闭当前弹窗以及所有堆叠在其之上的弹窗
  • dismissStack()
    仅关闭上层弹窗,保留当前弹窗可见
  • 可以用
    onDidFocus
    /
    onDidBlur
    监听弹窗获得/失去顶层位置的事件

Deep-Dive References

深度参考

When you need the full picture, load these reference files:
ReferenceWhat's inside
ConfigurationEvery prop with type, default, platform support, and notes
APIComplete events and methods reference with payload types
Advanced PatternsNavigation, Reanimated, Web, Side sheets, Liquid Glass, Jest mocking, Migration v2→v3
TroubleshootingCommon issues and fixes by platform
当你需要完整信息时,可以查看这些参考文件:
参考文档内容
配置说明所有属性的类型、默认值、平台支持和注意事项
API参考完整的事件和方法参考,包含参数类型
高级模式导航、Reanimated、Web、侧边弹窗、液态玻璃、Jest mock、v2→v3迁移
问题排查各平台常见问题和解决方案