# Installation (/install)
## Prerequisites [#prerequisites]
* React Native >= 0.80
* New Architecture enabled (default in RN 0.76+)
* Xcode 26.1+ (liquid glass support)
### Compatibility [#compatibility]
| TrueSheet | React Native | Expo SDK |
| --------- | ------------ | -------- |
| 3.7+ | 0.81-0.86 | 54-57 |
| 3.6 | 0.80 | 52-53 |
This package is not compatible with [Expo Go](https://docs.expo.dev/get-started/expo-go/). Use this with [Expo CNG](https://docs.expo.dev/workflow/continuous-native-generation/) instead.
## Expo [#expo]
```sh
npx expo install @lodev09/react-native-true-sheet
```
See [EAS Build troubleshooting](troubleshooting#eas-build-configuration) for build configuration.
## Bare React Native [#bare-react-native]
npm
pnpm
yarn
bun
```bash
npm install @lodev09/react-native-true-sheet
```
```bash
pnpm add @lodev09/react-native-true-sheet
```
```bash
yarn add @lodev09/react-native-true-sheet
```
```bash
bun add @lodev09/react-native-true-sheet
```
Then install the native dependencies:
```sh
cd ios && pod install
```
# Introduction (/intro)
The ***true*** native bottom sheet experience for your [React Native](https://reactnative.dev) Apps.
## Features [#features]
* ⚡ **Powered by Fabric** - Built on React Native's new architecture for maximum performance
* 🚀 **Fully Native** - Implemented in the native realm, zero JS hacks
* ♿ **Accessible** - Native accessibility and screen reader support out of the box
* 🔄 **Flexible API** - Use [imperative methods](reference/methods#ref-methods) or [lifecycle events](reference/events)
* ⌨️ **Keyboard Handling** - Built-in [keyboard handling](guides/keyboard) with automatic adjustment
* 🪟 **Liquid Glass** - [iOS 26+ Liquid Glass](guides/liquid-glass) support out of the box, featured in [Expo Blog](https://expo.dev/blog/how-to-create-apple-maps-style-liquid-glass-sheets)
* 🐎 **Reanimated** - First-class support for [react-native-reanimated](guides/reanimated)
* 🧭 **React Navigation** - Built-in [sheet navigator](guides/navigation) for seamless navigation integration
* 🌐 **Web Support** - Full [web support](guides/web) out of the box
# Migrating to v3 (/migration)
This guide will help you migrate from TrueSheet v2 to v3. Version 3 brings full Fabric (New Architecture) support and several improvements.
## Breaking Changes [#breaking-changes]
### 1. Fabric Architecture Required [#1-fabric-architecture-required]
Version 3 is built exclusively for React Native's **New Architecture (Fabric)**. The old Paper architecture is no longer supported.
**Requirements:**
* React Native >= 0.76 (Expo SDK 52+)
* New Architecture enabled (default in RN 0.76+)
If you're on React Native 0.76+, the New Architecture is enabled by default. No additional configuration is needed.
### 2. Prop Renames [#2-prop-renames]
Several props have been renamed for better clarity:
| v2 Prop | v3 Prop |
| ----------------------- | ------------------------------ |
| `sizes` | `detents` |
| `initialIndex` | `initialDetentIndex` |
| `initialIndexAnimated` | `initialDetentAnimated` |
| `contentContainerStyle` | *(removed - no longer needed)* |
| `FooterComponent` | `footer` |
| `edgeToEdge` | *(removed)* |
| `onPresent` | `onDidPresent` |
| `onDismiss` | `onDidDismiss` |
| `onSizeChange` | `onDetentChange` |
| `dimmedIndex` | `dimmedDetentIndex` |
**Migration:**
```tsx
// ❌ v2
}
edgeToEdge
onPresent={(e) => console.log(e.nativeEvent)}
onDismiss={() => console.log('dismissed')}
onSizeChange={(e) => console.log(e.nativeEvent)}
/>
// ✅ v3
}
onDidPresent={(e) => console.log(e.nativeEvent)}
onDidDismiss={() => console.log('dismissed')}
onDetentChange={(e) => console.log(e.nativeEvent)}
/>
```
### 3. Removed Props [#3-removed-props]
The following props have been removed in v3:
#### `grabberProps` (Android) [#grabberprops-android]
The `grabberProps` prop has been removed. The grabber is now rendered **natively** on both iOS and Android following Material Design 3 specifications. Use the `grabber` boolean prop to show or hide the native grabber.
**Migration:**
```tsx
// ❌ v2
{/* ... */}
// ✅ v3 - grabber is native, use boolean to show/hide
{/* ... */}
```
#### `scrollRef` (iOS) [#scrollref-ios]
The `scrollRef` prop has been removed. Scroll views are now **automatically detected** on iOS. Use the new `scrollable` prop to enable automatic ScrollView pinning.
**Migration:**
```tsx
// ❌ v2
const scrollViewRef = useRef(null)
{/* ... */}
// ✅ v3
{/* ... */}
```
See the [Scrolling guide](guides/scrolling) for more information about `scrollable`.
#### `contentContainerStyle` [#contentcontainerstyle]
The `contentContainerStyle` prop has been removed as it's no longer needed with the new architecture. Style your content directly instead.
### 4. Detent Value Changes [#4-detent-value-changes]
Detent values now use **fractional values (0-1)** instead of percentage strings.
**Migration:**
```tsx
// ❌ v2
// ✅ v3
```
The `"auto"` detent still works the same way!
### 5. Native Bottom Safe Area Handling [#5-native-bottom-safe-area-handling]
Both iOS and Android now handle bottom safe area insets natively for the sheet. This means the sheet height includes the safe area automatically, providing consistent behavior across all devices.
**What this means:**
* The `position`, `index`, and `detent` values are calculated purely from the actual sheet position relative to screen height
* A sheet at detent `0.5` will be taller than `0.5 * screenHeight` because the system adds the bottom safe area internally
**Footer with auto detent:**
The footer is pinned to the bottom edge of the sheet. When using an `auto` detent, the sheet height includes the safe area, so your footer needs to extend into it:
```tsx
import { Platform } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const isIPad = Platform.OS === 'ios' && Platform.isPad;
const MyFooter = () => {
const insets = useSafeAreaInsets();
const bottomInset = isIPad ? 0 : insets.bottom;
return (
Footer Content
);
};
// Usage
}>
{/* content */}
```
This ensures the footer background extends into the safe area while keeping the content above the home indicator.
On iPad, the sheet is displayed as a floating modal, so bottom padding is not needed.
See the [Footer guide](guides/footer) for more details.
### 6. Background and Blur Behavior Changes [#6-background-and-blur-behavior-changes]
#### `backgroundColor` Default Value [#backgroundcolor-default-value]
The `backgroundColor` prop no longer defaults to `"white"`. It now uses the **system default** when not provided, which matches the platform's native sheet background.
**Migration:**
```tsx
// ❌ v2 - backgroundColor defaults to white
// ✅ v3 - backgroundColor uses system default (transparent on iOS)
// If you want white background, explicitly set it:
```
#### `backgroundBlur` No Longer Overrides `backgroundColor` [#backgroundblur-no-longer-overrides-backgroundcolor]
In v2, setting `backgroundBlur` (previously `blurTint`) would completely override the `backgroundColor`. In v3, `backgroundBlur` now **applies a blur effect over** the `backgroundColor`, allowing you to combine both for richer visual effects.
**Migration:**
```tsx
// ❌ v2 - backgroundBlur overrides backgroundColor (backgroundColor is ignored)
// ✅ v3 - backgroundBlur applies over backgroundColor (red + light blur)
// If you want only blur without background color:
```
## New Features in v3 [#new-features-in-v3]
### 1. Automatic Edge-to-Edge Detection (Android) [#1-automatic-edge-to-edge-detection-android]
TrueSheet now automatically detects and adapts to Android's edge-to-edge display mode. The sheet respects the status bar when fully expanded.
This feature is **Android-only** and requires React Native 0.81+ with `edgeToEdgeEnabled=true` in your `android/gradle.properties`.
See the [Edge-to-Edge guide](guides/edge-to-edge) for details.
### 2. Enhanced Event System [#2-enhanced-event-system]
Version 3 introduces new lifecycle and interaction events for better control:
#### New Events: [#new-events]
* **`onMount`** - Called when the sheet's content is mounted and ready
* **`onWillPresent`** - Called when the sheet is about to be presented
* **`onDidPresent`** - Called when the sheet has been presented
* **`onWillDismiss`** - Called when the sheet is about to be dismissed
* **`onDidDismiss`** - Called when the sheet has been dismissed
* **`onDragBegin`** - Called when the sheet begins dragging
* **`onDragChange`** - Called continuously while the sheet is being dragged
* **`onDragEnd`** - Called when sheet dragging has ended
* **`onPositionChange`** - Called continuously when the sheet's position changes
* **`onWillFocus`** - Called when the sheet is about to regain focus after a sheet on top begins dismissing
* **`onDidFocus`** - Called when the sheet has regained focus after a sheet on top is dismissed
* **`onWillBlur`** - Called when the sheet is about to lose focus because another sheet is about to be presented on top
* **`onDidBlur`** - Called when the sheet has lost focus because another sheet is presented on top
These events provide more granular control over the sheet's lifecycle and user interactions.
See the [Lifecycle Events documentation](reference/events) for detailed usage and examples.
### 3. iPad Support with Presentation [#3-ipad-support-with-presentation]
Version 3 now includes first-class iPad support with the new `presentation` prop. `'page'` (default) uses a large page sheet for better readability on iPad. `'form'` uses a centered floating form sheet.
```tsx
```
This feature is **iOS 17+ only** and defaults to `'page'`. Set to `'form'` to use a centered form sheet presentation on iPad.
### 4. Improved Performance [#4-improved-performance]
* Faster rendering with Fabric architecture
* Built-in lazy loading by default
* Smoother animations and transitions
### 5. First-Class Reanimated v4 Support [#5-first-class-reanimated-v4-support]
Version 3 introduces dedicated components for seamless integration with `react-native-reanimated` v4:
#### New Components: [#new-components]
* **`ReanimatedTrueSheetProvider`** - Context provider for managing shared values
* **`ReanimatedTrueSheet`** - Drop-in replacement for `TrueSheet` with automatic position tracking
* **`useReanimatedTrueSheet`** - Hook to access the sheet's animated position
#### Example: [#example]
```tsx
import {
ReanimatedTrueSheetProvider,
ReanimatedTrueSheet,
useReanimatedTrueSheet,
} from '@lodev09/react-native-true-sheet/reanimated'
import Animated, { useAnimatedStyle, interpolate, Extrapolation } from 'react-native-reanimated'
// 1. Wrap your app with the provider
const App = () => (
)
// 2. Use ReanimatedTrueSheet instead of TrueSheet
const MySheet = () => (
Sheet Content
)
// 3. Access the sheet's position for custom animations
const MyAnimatedComponent = () => {
const { animatedPosition, animatedIndex } = useReanimatedTrueSheet()
const positionStyle = useAnimatedStyle(() => ({
transform: [{ translateY: -animatedPosition.value }],
}))
// Example: Fade in as sheet expands from index 0 to 1
const opacityStyle = useAnimatedStyle(() => ({
opacity: interpolate(animatedIndex.value, [0, 1], [0, 1], Extrapolation.CLAMP)
}))
return (
This moves with the sheet
)
}
```
The `onPositionChange` event now runs on the UI thread (worklet) when using `ReanimatedTrueSheet`. Make sure to add the `'worklet'` directive to your handler if you override it.
See the [Reanimated Integration guide](guides/reanimated) for complete examples.
### 6. Native Header Support [#6-native-header-support]
Version 3 introduces a new `header` prop for adding fixed headers above your scrollable content. The header is rendered as a native view and is properly accounted for in layout calculations.
```tsx
}>
```
This ensures proper scrolling behavior on both iOS and Android when using `ScrollView` or `FlatList` with a header.
See the [Header guide](guides/header) for more details.
### 7. Draggable Control [#7-draggable-control]
Version 3 introduces the `draggable` prop to disable user dragging entirely. When set to `false`, the sheet becomes static and can only be resized programmatically using the [`resize`](reference/methods#resize) method.
```tsx
```
When `draggable` is `false`, the grabber is automatically hidden.
See the [Configuration reference](reference/configuration#draggable) for more details.
### 8. Blur Intensity and Interaction Control [#8-blur-intensity-and-interaction-control]
Version 3 introduces new props for fine-grained control over the blur effect on iOS:
* **`blurIntensity`** - Control the blur intensity (0-100)
* **`blurInteraction`** - Enable/disable user interaction on the blur view
```tsx
```
See the [Configuration reference](reference/configuration#bluroptions) for more details.
### 9. React Native Screens Integration [#9-react-native-screens-integration]
Version 3 works seamlessly with React Navigation! You can navigate to other screens from within a sheet and the sheet will remain visible in the background when presenting modals on top.
This requires changes to `react-native-screens`. There is a [pending PR](https://github.com/software-mansion/react-native-screens/pull/3415) that adds support for this. In the meantime, you can apply the patch from the [example app](https://github.com/lodev09/react-native-true-sheet/blob/main/.yarn/patches/react-native-screens-npm-4.25.2.patch).
```tsx
const sheet = useRef(null)
const navigate = () => {
// Navigate directly - no need to dismiss first!
navigation.navigate('SomeScreen')
}
return (
)
```
See the [React Navigation guide](guides/navigation) for more details.
## Step-by-Step Migration [#step-by-step-migration]
1. **Enable New Architecture** in your React Native app (see above)
2. **Update the package:**
npm
pnpm
yarn
bun
```bash
npm install @lodev09/react-native-true-sheet@^3.0.0
```
```bash
pnpm add @lodev09/react-native-true-sheet@^3.0.0
```
```bash
yarn add @lodev09/react-native-true-sheet@^3.0.0
```
```bash
bun add @lodev09/react-native-true-sheet@^3.0.0
```
3. **Clean and reinstall iOS dependencies:**
```sh
cd ios
rm -rf Pods Podfile.lock build
pod install
cd ..
```
4. **Clean Android build:**
```sh
cd android
./gradlew clean
cd ..
```
5. **Update your code:**
* Replace `sizes` with `detents`
* Replace `initialIndex` with `initialDetentIndex`
* Replace `initialIndexAnimated` with `initialDetentAnimated`
* Replace `FooterComponent` with `footer`
* Remove `edgeToEdge` prop (no longer needed)
* Replace `onPresent` with `onDidPresent`
* Replace `onDismiss` with `onDidDismiss`
* Replace `onSizeChange` with `onDetentChange`
* Replace `dimmedIndex` with `dimmedDetentIndex`
* Remove `scrollRef` prop (iOS - now auto-detected)
* Remove `contentContainerStyle` prop (no longer needed)
* Remove `grabberProps` prop (grabber is now native on both platforms)
* Change detent percentage strings to fractional numbers (e.g., `"50%"` → `0.5`)
6. **Test your app:**
```sh
npx react-native run-ios
npx react-native run-android
```
## Need Help? [#need-help]
If you encounter any issues during migration:
* Check the [Troubleshooting](troubleshooting) guide
* Open an issue on [GitHub](https://github.com/lodev09/react-native-true-sheet/issues)
* Review the [example app](https://github.com/lodev09/react-native-true-sheet/tree/main/example) for reference implementations
# Troubleshooting (/troubleshooting)
## Blank Screen When Dismissing React Native Modal on iOS [#blank-screen-when-dismissing-react-native-modal-on-ios]
When presenting a TrueSheet on top of a React Native `` and then dismissing the Modal (by setting `visible={false}`), the screen may go blank. This is a bug in React Native where dismissing a Modal that has another view controller presented on top of it only dismisses the topmost view controller, leaving the Modal in an inconsistent state.
**Workaround:**
Dismiss the sheet first before hiding the Modal:
```tsx
const sheet = useRef(null)
const [modalVisible, setModalVisible] = useState(false)
const closeModal = async () => {
await sheet.current?.dismiss()
setModalVisible(false)
}
```
A fix has been submitted to React Native. See [PR #55005](https://github.com/facebook/react-native/pull/55005).
## Sheet Snaps Without Animation on Keyboard Dismiss (iOS) [#sheet-snaps-without-animation-on-keyboard-dismiss-ios]
On React Native 0.83 through 0.85, the sheet snaps back to its detent without animation when the keyboard is dismissed. This is caused by an upstream change in RN 0.83 where `canBecomeFirstResponder` returns `YES` on every `RCTViewComponentView`, disrupting `UISheetPresentationController`'s keyboard avoidance animation.
**Workaround:**
Apply a [patch](https://github.com/lodev09/react-native-true-sheet/blob/main/.yarn/patches/react-native-npm-0.85.3.patch) to `react-native` that gates `canBecomeFirstResponder` behind the `enableImperativeFocus` feature flag. For Yarn, add to your `package.json`:
```json
"resolutions": {
"react-native": "patch:react-native@npm%3A0.85.3#./.yarn/patches/react-native-npm-0.85.3.patch"
}
```
Fixed upstream in React Native 0.86. The patch is only needed on RN 0.83–0.85; remove it once you upgrade to 0.86+.
## Gesture Handler Not Working on Android [#gesture-handler-not-working-on-android]
If you're using `react-native-gesture-handler` inside the sheet and gestures are not working on Android, you need to wrap your sheet content with `GestureHandlerRootView`.
```tsx
import { GestureHandlerRootView } from 'react-native-gesture-handler'
{/* Your content with gestures */}
```
Use `flexGrow: 1` instead of `flex: 1` to avoid layout issues.
[Learn more](https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/installation#android) about gesture handler setup on Android.
## Initial Present and Deep Linking [#initial-present-and-deep-linking]
On iOS, `initialDetentIndex` may not work when deep linking to a modal screen from a cold start. When navigating back to the home screen, the sheet won't present because the view wasn't attached to the correct window during the initial render.
**Workaround:**
Use `useFocusEffect` to present the sheet when the screen gains focus, conditioned on whether the initial present failed:
```tsx
import { useFocusEffect } from '@react-navigation/native';
import { useCallback } from 'react';
function HomeScreen() {
const sheet = useRef(null);
const [didPresent, setDidPresent] = useState(false);
useFocusEffect(
useCallback(() => {
if (!didPresent) {
sheet.current?.present();
}
}, [didPresent])
);
return (
setDidPresent(true)}
>
{/* ... */}
);
}
```
## Weird layout render [#weird-layout-render]
The sheet does not have control over how React Native renders components and may lead to rendering issues. To resolve this, try to minimize the use of `flex=1` in your content styles. Instead, use fixed `height` or employ `flexGrow`, `flexBasis`, etc., to manage your layout requirements.
Try moving the sheet component up to a higher level in the component tree as some container may interfere with the sheet's content rendering.
## Xcode and EAS Build Issues [#xcode-and-eas-build-issues]
### Xcode Version [#xcode-version]
The package requires **Xcode 26.1 or later**. Background functionality may not work correctly with older versions. Check your Xcode version:
```sh
xcodebuild -version
```
### EAS Build Configuration [#eas-build-configuration]
When using [EAS Build](https://docs.expo.dev/build/introduction/), configure your `eas.json` to use a build image with Xcode 26.1+. Use `"image": "latest"` or choose from the [available build images](https://docs.expo.dev/build-reference/infrastructure/#ios-server-images):
```json
{
"build": {
"production": {
"ios": {
"image": "latest"
}
},
"development": {
"ios": {
"image": "latest"
}
}
}
}
```
### Patched `react-native-screens` Not Applied on EAS (Precompiled Modules) [#patched-react-native-screens-not-applied-on-eas-precompiled-modules]
If you apply the [`react-native-screens` patch](guides/navigation) so the sheet stays visible when presenting screens on top, you may find that on **Expo SDK 56+ EAS builds** the sheet dismisses — or a full-screen modal fails to present over the sheet — even though it works in a local build.
This is because Expo SDK 56 ships `react-native-screens` (and other common libraries like `react-native-reanimated`, `react-native-svg`) as **precompiled XCFrameworks** on EAS. The prebuilt framework is built from the library's own source, so any patch you apply to `react-native-screens` (via `patch-package`, `pnpm patch`, or Yarn `patches`) is **never compiled in** — it's silently ignored on EAS, while local builds still work because they fall back to compiling from source.
**Fix:**
Disable precompiled modules so `react-native-screens` builds from your patched source. Add the env var to each build profile in `eas.json`:
```json
{
"build": {
"development": { "env": { "EXPO_USE_PRECOMPILED_MODULES": "0" } },
"preview": { "env": { "EXPO_USE_PRECOMPILED_MODULES": "0" } },
"production": { "env": { "EXPO_USE_PRECOMPILED_MODULES": "0" } }
}
}
```
This only disables **module** precompilation — React Native core stays prebuilt, so build times are largely unaffected.
To keep precompiled modules for everything else and only build `react-native-screens` from source, opt it out per-package in `package.json` instead:
```json
{
"expo": {
"autolinking": {
"ios": { "buildFromSource": ["react-native-screens"] }
}
}
}
```
Avoid `ios.buildReactNativeFromSource: true` for this — it also rebuilds React Native core from source, which is much slower.
# Usage (/usage)
1. Import `TrueSheet`.
```tsx
import { TrueSheet } from "@lodev09/react-native-true-sheet"
```
2. Define `TrueSheet` inside any component and attach a `ref` to it.
3. Control the sheet by invoking the available `ref` [methods](reference/methods).
```tsx {2-2,5-8,11-14,18-25}
export const App = () => {
const sheet = useRef(null)
// Present the sheet ✅
const present = async () => {
await sheet.current?.present()
console.log('horray! sheet has been presented 💩')
}
// Dismiss the sheet ✅
const dismiss = async () => {
await sheet.current?.dismiss()
console.log('Bye bye 👋')
}
return (
)
}
```
Simple, right? Head over to [Configuration](reference/configuration) to learn more about configuring your sheet.
## Skills [#skills]
Skills are reusable AI capabilities that give your AI coding agent knowledge about TrueSheet. With the right skill loaded, your agent can pick the right patterns, avoid common mistakes, and generate correct code without you having to explain the library every time.
```sh
npx skills add lodev09/react-native-true-sheet
```
This will install the **TrueSheet Usage** skill into your project.
# Installation (/next/install)
## Prerequisites [#prerequisites]
* React Native >= 0.82
* New Architecture enabled (default in RN 0.76+)
* Xcode 26.1+ (liquid glass support)
### Compatibility [#compatibility]
| TrueSheet | React Native | Expo SDK |
| ---------- | ------------ | -------- |
| 4.0+ | 0.82+ | 55+ |
| 3.7 - 3.11 | 0.81-0.86 | 54-57 |
| 3.6 | 0.80 | 52-53 |
This package is not compatible with [Expo Go](https://docs.expo.dev/get-started/expo-go/). Use this with [Expo CNG](https://docs.expo.dev/workflow/continuous-native-generation/) instead.
## Expo [#expo]
```sh
npx expo install @lodev09/react-native-true-sheet
```
See [EAS Build troubleshooting](troubleshooting#eas-build-configuration) for build configuration.
## Bare React Native [#bare-react-native]
npm
pnpm
yarn
bun
```bash
npm install @lodev09/react-native-true-sheet
```
```bash
pnpm add @lodev09/react-native-true-sheet
```
```bash
yarn add @lodev09/react-native-true-sheet
```
```bash
bun add @lodev09/react-native-true-sheet
```
Then install the native dependencies:
```sh
cd ios && pod install
```
# Introduction (/next/intro)
The ***true*** native bottom sheet experience for your [React Native](https://reactnative.dev) Apps.
## Features [#features]
* ⚡ **Powered by Fabric** - Built on React Native's new architecture for maximum performance
* 🚀 **Fully Native** - Implemented in the native realm, zero JS hacks
* ♿ **Accessible** - Native accessibility and screen reader support out of the box
* 🔄 **Flexible API** - Use [imperative methods](reference/methods#ref-methods) or [lifecycle events](reference/events)
* ⌨️ **Keyboard Handling** - Built-in [keyboard handling](guides/keyboard) with automatic adjustment
* 🪟 **Liquid Glass** - [iOS 26+ Liquid Glass](guides/liquid-glass) support out of the box, featured in [Expo Blog](https://expo.dev/blog/how-to-create-apple-maps-style-liquid-glass-sheets)
* 🐎 **Reanimated** - First-class support for [react-native-reanimated](guides/reanimated)
* 🧭 **React Navigation** - Built-in [sheet navigator](guides/navigation) for seamless navigation integration
* 🌐 **Web Support** - Full [web support](guides/web) out of the box
# Migrating to v3 (/next/migration-v3)
This guide will help you migrate from TrueSheet v2 to v3. Version 3 brings full Fabric (New Architecture) support and several improvements.
Upgrading from v3? See [Migrating to v4](migration).
## Breaking Changes [#breaking-changes]
### 1. Fabric Architecture Required [#1-fabric-architecture-required]
Version 3 is built exclusively for React Native's **New Architecture (Fabric)**. The old Paper architecture is no longer supported.
**Requirements:**
* React Native >= 0.76 (Expo SDK 52+)
* New Architecture enabled (default in RN 0.76+)
If you're on React Native 0.76+, the New Architecture is enabled by default. No additional configuration is needed.
### 2. Prop Renames [#2-prop-renames]
Several props have been renamed for better clarity:
| v2 Prop | v3 Prop |
| ----------------------- | ------------------------------ |
| `sizes` | `detents` |
| `initialIndex` | `initialDetentIndex` |
| `initialIndexAnimated` | `initialDetentAnimated` |
| `contentContainerStyle` | *(removed - no longer needed)* |
| `FooterComponent` | `footer` |
| `edgeToEdge` | *(removed)* |
| `onPresent` | `onDidPresent` |
| `onDismiss` | `onDidDismiss` |
| `onSizeChange` | `onDetentChange` |
| `dimmedIndex` | `dimmedDetentIndex` |
**Migration:**
```tsx
// ❌ v2
}
edgeToEdge
onPresent={(e) => console.log(e.nativeEvent)}
onDismiss={() => console.log('dismissed')}
onSizeChange={(e) => console.log(e.nativeEvent)}
/>
// ✅ v3
}
onDidPresent={(e) => console.log(e.nativeEvent)}
onDidDismiss={() => console.log('dismissed')}
onDetentChange={(e) => console.log(e.nativeEvent)}
/>
```
### 3. Removed Props [#3-removed-props]
The following props have been removed in v3:
#### `grabberProps` (Android) [#grabberprops-android]
The `grabberProps` prop has been removed. The grabber is now rendered **natively** on both iOS and Android following Material Design 3 specifications. Use the `grabber` boolean prop to show or hide the native grabber.
**Migration:**
```tsx
// ❌ v2
{/* ... */}
// ✅ v3 - grabber is native, use boolean to show/hide
{/* ... */}
```
#### `scrollRef` (iOS) [#scrollref-ios]
The `scrollRef` prop has been removed. Scroll views are now **automatically detected** on iOS. Use the new `scrollable` prop to enable automatic ScrollView pinning.
**Migration:**
```tsx
// ❌ v2
const scrollViewRef = useRef(null)
{/* ... */}
// ✅ v3
{/* ... */}
```
The `scrollable` prop was later removed in v4 — see [Migrating to v4](migration#2-scrollable-prop-removed).
#### `contentContainerStyle` [#contentcontainerstyle]
The `contentContainerStyle` prop has been removed as it's no longer needed with the new architecture. Style your content directly instead.
### 4. Detent Value Changes [#4-detent-value-changes]
Detent values now use **fractional values (0-1)** instead of percentage strings.
**Migration:**
```tsx
// ❌ v2
// ✅ v3
```
The `"auto"` detent still works the same way!
### 5. Native Bottom Safe Area Handling [#5-native-bottom-safe-area-handling]
Both iOS and Android now handle bottom safe area insets natively for the sheet. This means the sheet height includes the safe area automatically, providing consistent behavior across all devices.
**What this means:**
* The `position`, `index`, and `detent` values are calculated purely from the actual sheet position relative to screen height
* A sheet at detent `0.5` will be taller than `0.5 * screenHeight` because the system adds the bottom safe area internally
**Footer with auto detent:**
The footer is pinned to the bottom edge of the sheet. When using an `auto` detent, the sheet height includes the safe area, so your footer needs to extend into it:
```tsx
import { Platform } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const isIPad = Platform.OS === 'ios' && Platform.isPad;
const MyFooter = () => {
const insets = useSafeAreaInsets();
const bottomInset = isIPad ? 0 : insets.bottom;
return (
Footer Content
);
};
// Usage
}>
{/* content */}
```
This ensures the footer background extends into the safe area while keeping the content above the home indicator.
On iPad, the sheet is displayed as a floating modal, so bottom padding is not needed.
See the [Footer guide](guides/footer) for more details.
### 6. Background and Blur Behavior Changes [#6-background-and-blur-behavior-changes]
#### `backgroundColor` Default Value [#backgroundcolor-default-value]
The `backgroundColor` prop no longer defaults to `"white"`. It now uses the **system default** when not provided, which matches the platform's native sheet background.
**Migration:**
```tsx
// ❌ v2 - backgroundColor defaults to white
// ✅ v3 - backgroundColor uses system default (transparent on iOS)
// If you want white background, explicitly set it:
```
#### `backgroundBlur` No Longer Overrides `backgroundColor` [#backgroundblur-no-longer-overrides-backgroundcolor]
In v2, setting `backgroundBlur` (previously `blurTint`) would completely override the `backgroundColor`. In v3, `backgroundBlur` now **applies a blur effect over** the `backgroundColor`, allowing you to combine both for richer visual effects.
**Migration:**
```tsx
// ❌ v2 - backgroundBlur overrides backgroundColor (backgroundColor is ignored)
// ✅ v3 - backgroundBlur applies over backgroundColor (red + light blur)
// If you want only blur without background color:
```
## New Features in v3 [#new-features-in-v3]
### 1. Automatic Edge-to-Edge Detection (Android) [#1-automatic-edge-to-edge-detection-android]
TrueSheet now automatically detects and adapts to Android's edge-to-edge display mode. The sheet respects the status bar when fully expanded.
This feature is **Android-only** and requires React Native 0.81+ with `edgeToEdgeEnabled=true` in your `android/gradle.properties`.
See the [Edge-to-Edge guide](guides/edge-to-edge) for details.
### 2. Enhanced Event System [#2-enhanced-event-system]
Version 3 introduces new lifecycle and interaction events for better control:
#### New Events: [#new-events]
* **`onMount`** - Called when the sheet's content is mounted and ready
* **`onWillPresent`** - Called when the sheet is about to be presented
* **`onDidPresent`** - Called when the sheet has been presented
* **`onWillDismiss`** - Called when the sheet is about to be dismissed
* **`onDidDismiss`** - Called when the sheet has been dismissed
* **`onDragBegin`** - Called when the sheet begins dragging
* **`onDragChange`** - Called continuously while the sheet is being dragged
* **`onDragEnd`** - Called when sheet dragging has ended
* **`onPositionChange`** - Called continuously when the sheet's position changes
* **`onWillFocus`** - Called when the sheet is about to regain focus after a sheet on top begins dismissing
* **`onDidFocus`** - Called when the sheet has regained focus after a sheet on top is dismissed
* **`onWillBlur`** - Called when the sheet is about to lose focus because another sheet is about to be presented on top
* **`onDidBlur`** - Called when the sheet has lost focus because another sheet is presented on top
These events provide more granular control over the sheet's lifecycle and user interactions.
See the [Lifecycle Events documentation](reference/events) for detailed usage and examples.
### 3. iPad Support with Presentation [#3-ipad-support-with-presentation]
Version 3 now includes first-class iPad support with the new `presentation` prop. `'page'` (default) uses a large page sheet for better readability on iPad. `'form'` uses a centered floating form sheet.
```tsx
```
This feature is **iOS 17+ only** and defaults to `'page'`. Set to `'form'` to use a centered form sheet presentation on iPad.
### 4. Improved Performance [#4-improved-performance]
* Faster rendering with Fabric architecture
* Built-in lazy loading by default
* Smoother animations and transitions
### 5. First-Class Reanimated v4 Support [#5-first-class-reanimated-v4-support]
Version 3 introduces dedicated components for seamless integration with `react-native-reanimated` v4:
#### New Components: [#new-components]
* **`ReanimatedTrueSheetProvider`** - Context provider for managing shared values
* **`ReanimatedTrueSheet`** - Drop-in replacement for `TrueSheet` with automatic position tracking
* **`useReanimatedTrueSheet`** - Hook to access the sheet's animated position
#### Example: [#example]
```tsx
import {
ReanimatedTrueSheetProvider,
ReanimatedTrueSheet,
useReanimatedTrueSheet,
} from '@lodev09/react-native-true-sheet/reanimated'
import Animated, { useAnimatedStyle, interpolate, Extrapolation } from 'react-native-reanimated'
// 1. Wrap your app with the provider
const App = () => (
)
// 2. Use ReanimatedTrueSheet instead of TrueSheet
const MySheet = () => (
Sheet Content
)
// 3. Access the sheet's position for custom animations
const MyAnimatedComponent = () => {
const { animatedPosition, animatedIndex } = useReanimatedTrueSheet()
const positionStyle = useAnimatedStyle(() => ({
transform: [{ translateY: -animatedPosition.value }],
}))
// Example: Fade in as sheet expands from index 0 to 1
const opacityStyle = useAnimatedStyle(() => ({
opacity: interpolate(animatedIndex.value, [0, 1], [0, 1], Extrapolation.CLAMP)
}))
return (
This moves with the sheet
)
}
```
The `onPositionChange` event now runs on the UI thread (worklet) when using `ReanimatedTrueSheet`. Make sure to add the `'worklet'` directive to your handler if you override it.
See the [Reanimated Integration guide](guides/reanimated) for complete examples.
### 6. Native Header Support [#6-native-header-support]
Version 3 introduces a new `header` prop for adding fixed headers above your scrollable content. The header is rendered as a native view and is properly accounted for in layout calculations.
```tsx
}>
```
This ensures proper scrolling behavior on both iOS and Android when using `ScrollView` or `FlatList` with a header.
See the [Header guide](guides/header) for more details.
### 7. Draggable Control [#7-draggable-control]
Version 3 introduces the `draggable` prop to disable user dragging entirely. When set to `false`, the sheet becomes static and can only be resized programmatically using the [`resize`](reference/methods#resize) method.
```tsx
```
When `draggable` is `false`, the grabber is automatically hidden.
See the [Configuration reference](reference/configuration#draggable) for more details.
### 8. Blur Intensity and Interaction Control [#8-blur-intensity-and-interaction-control]
Version 3 introduces new props for fine-grained control over the blur effect on iOS:
* **`blurIntensity`** - Control the blur intensity (0-100)
* **`blurInteraction`** - Enable/disable user interaction on the blur view
```tsx
```
In v4, `blurOptions` is removed. See [Migrating to v4](migration#11-bluroptions-removed) for its replacement.
### 9. React Native Screens Integration [#9-react-native-screens-integration]
Version 3 works seamlessly with React Navigation! You can navigate to other screens from within a sheet and the sheet will remain visible in the background when presenting modals on top.
This requires changes to `react-native-screens`. There is a [pending PR](https://github.com/software-mansion/react-native-screens/pull/3415) that adds support for this. In the meantime, you can apply the patch from the [example app](https://github.com/lodev09/react-native-true-sheet/blob/main/.yarn/patches/react-native-screens-npm-4.25.2.patch).
```tsx
const sheet = useRef(null)
const navigate = () => {
// Navigate directly - no need to dismiss first!
navigation.navigate('SomeScreen')
}
return (
)
```
See the [React Navigation guide](guides/navigation) for more details.
## Step-by-Step Migration [#step-by-step-migration]
1. **Enable New Architecture** in your React Native app (see above)
2. **Update the package:**
npm
pnpm
yarn
bun
```bash
npm install @lodev09/react-native-true-sheet@^3.0.0
```
```bash
pnpm add @lodev09/react-native-true-sheet@^3.0.0
```
```bash
yarn add @lodev09/react-native-true-sheet@^3.0.0
```
```bash
bun add @lodev09/react-native-true-sheet@^3.0.0
```
3. **Clean and reinstall iOS dependencies:**
```sh
cd ios
rm -rf Pods Podfile.lock build
pod install
cd ..
```
4. **Clean Android build:**
```sh
cd android
./gradlew clean
cd ..
```
5. **Update your code:**
* Replace `sizes` with `detents`
* Replace `initialIndex` with `initialDetentIndex`
* Replace `initialIndexAnimated` with `initialDetentAnimated`
* Replace `FooterComponent` with `footer`
* Remove `edgeToEdge` prop (no longer needed)
* Replace `onPresent` with `onDidPresent`
* Replace `onDismiss` with `onDidDismiss`
* Replace `onSizeChange` with `onDetentChange`
* Replace `dimmedIndex` with `dimmedDetentIndex`
* Remove `scrollRef` prop (iOS - now auto-detected)
* Remove `contentContainerStyle` prop (no longer needed)
* Remove `grabberProps` prop (grabber is now native on both platforms)
* Change detent percentage strings to fractional numbers (e.g., `"50%"` → `0.5`)
6. **Test your app:**
```sh
npx react-native run-ios
npx react-native run-android
```
## Need Help? [#need-help]
If you encounter any issues during migration:
* Check the [Troubleshooting](troubleshooting) guide
* Open an issue on [GitHub](https://github.com/lodev09/react-native-true-sheet/issues)
* Review the [example app](https://github.com/lodev09/react-native-true-sheet/tree/main/example) for reference implementations
# Migrating to v4 (/next/migration)
This guide will help you migrate from TrueSheet v3 to v4. Version 4 rewrites the sheet's layout engine — the sheet now lays out synchronously per detent with Yoga owning all frames, so the container is sized to the sheet's visible height and tracks it in realtime while dragging.
Upgrading from v2? Start with [Migrating to v3](migration-v3).
## Breaking Changes [#breaking-changes]
### 1. React Native 0.82+ Required [#1-react-native-082-required]
Version 4 relies on synchronous Fabric state updates (available since React Native 0.82) to resize content in the same frame as the sheet.
**Requirements:**
* React Native >= 0.82 (Expo SDK 55+)
* New Architecture enabled
### 2. `scrollable` Prop Replaced by `scrollableRef` [#2-scrollable-prop-replaced-by-scrollableref]
Point the sheet at your scroll view (including `ScrollView` and `FlatList`) with the `scrollableRef` prop. Like any scroll view in React Native, it needs a bounded height to scroll, so pass `flex: 1` via the sheet's `style` prop for fixed detents. With an `auto` detent, no `flex: 1` is needed — the sheet sizes to the scroll content and bounds the viewport automatically.
**Migration:**
```tsx
// ❌ v3
{/* ... */}
// ✅ v4 — plug the scroll view via scrollableRef, bound the content with flex: 1
{/* ... */}
// ✅ v4 — 'auto' detent sizes to the scroll content, no flex needed
{/* ... */}
```
See the [Scrolling guide](guides/scrolling) for more information.
### 3. Content Lays Out Naturally [#3-content-lays-out-naturally]
Content now wraps its children's height by default — like a regular view or a react-navigation screen — instead of filling the sheet. If your layout relied on the content filling the sheet (e.g. spacers, centered content, or a bounded scroll view), pass `flex: 1` via the sheet's `style` prop.
**Migration:**
```tsx
// ❌ v3 — content filled the sheet implicitly
Centered
// ✅ v4 — fill the sheet explicitly
Centered
```
### 4. Footer Lays Out Relative by Default [#4-footer-lays-out-relative-by-default]
The footer now takes up space **below the content** (still pinned to the sheet's bottom edge). Its height is included in the `auto` detent calculation, but excluded from the `peek` detent (it's pushed off-screen at peek). `footerOptions.keyboardOffset` no longer applies — a relative footer stays in the layout flow behind the keyboard.
To restore the v3 floating behavior, set the new `footerOptions.position` to `'absolute'` — the footer floats over the content, is excluded from the `auto` detent, counts toward the `peek` detent, and rises above the keyboard.
**Migration:**
```tsx
// ❌ v3 — footer floats over the content
}>
{/* content */}
// ✅ v4 — same floating behavior, now opt-in
} footerOptions={{ position: 'absolute' }}>
{/* content */}
```
A relative footer is laid out below the content — if your content is taller than the sheet's visible height (e.g. fixed detents), bound it with `flex: 1` so the footer stays visible.
See the [Footer guide](guides/footer) for more details.
### 5. Footer Absorbs the Bottom Safe-Area Inset [#5-footer-absorbs-the-bottom-safe-area-inset]
The footer now owns the sheet's bottom edge and absorbs the bottom safe-area inset as padding when `insetAdjustment` is `"automatic"` (the default) — its content stays above the home indicator while its background fills the inset. Remove any manual safe-area padding from your footer, or it will be applied twice:
```tsx
// ❌ v3 — manual safe-area padding
const MyFooter = () => {
const insets = useSafeAreaInsets();
return (
);
};
// ✅ v4 — the footer absorbs the inset natively
} footerStyle={{ backgroundColor: '#333' }}>
{/* content */}
```
See the [Footer guide](guides/footer#safe-area-handling) for more details.
### 6. Scrollable Bottom Inset via `contentInsetAdjustment` [#6-scrollable-bottom-inset-via-contentinsetadjustment]
A plugged scrollable still gets the bottom safe-area inset applied natively, now only while the content can actually scroll — mirroring iOS's [`contentInsetAdjustmentBehavior="automatic"`](https://developer.apple.com/documentation/uikit/uiscrollview/contentinsetadjustmentbehavior) on all platforms. An absolute footer floating over the scrollable is handled the same way: the scroll content is padded by the footer's measured height and the footer counts toward the `auto` detent. Remove any manual safe-area or footer padding from your scroll content, or opt out with the new `scrollableOptions.contentInsetAdjustment` and pad it yourself:
```tsx
// ✅ v4 — the safe-area and footer insets are applied natively
} footerOptions={{ position: 'absolute' }}>
{/* ... */}
// ✅ v4 — opt out and pad the content yourself
const insets = useSafeAreaInsets();
{/* ... */}
```
`'safe-area'` keeps only the safe-area inset (content scrolls under the footer) and `'footer'` keeps only the footer inset. See [Scrolling Content](guides/scrolling#bottom-safe-area-inset).
If your sheet has a relative footer, it absorbs the bottom inset instead (see above) — the scroll view ends above it, so no padding is needed.
### 7. Sheet Navigator Requires `@react-navigation/native` 7.3+ [#7-sheet-navigator-requires-react-navigationnative-73]
The navigator is now built on [`standard-navigation`](https://github.com/react-navigation/standard-navigation), so one implementation works with both React Navigation and Expo Router. For React Navigation apps, the `/navigation` entry point now requires `@react-navigation/native` version `7.3.0` or higher (previously `@react-navigation/core`), plus the new `standard-navigation` peer dependency:
npm
pnpm
yarn
bun
```bash
npm install @react-navigation/native@^7.3.0 standard-navigation
```
```bash
pnpm add @react-navigation/native@^7.3.0 standard-navigation
```
```bash
yarn add @react-navigation/native@^7.3.0 standard-navigation
```
```bash
bun add @react-navigation/native@^7.3.0 standard-navigation
```
Your navigator code is unchanged — `createTrueSheetNavigator`, `useTrueSheetNavigation`, screen options, `navigation.resize()`, and listeners all keep the same API.
### 8. Expo Router Uses the New `/navigation/expo-router` Entry Point [#8-expo-router-uses-the-new-navigationexpo-router-entry-point]
The `withLayoutContext` recipe is replaced by a ready-to-use `Sheet` layout. Requires Expo SDK 57+ and the new `standard-navigation` peer dependency — no `@react-navigation/*` install needed.
npm
pnpm
yarn
bun
```bash
npm install standard-navigation
```
```bash
pnpm add standard-navigation
```
```bash
yarn add standard-navigation
```
```bash
bun add standard-navigation
```
**Migration:**
```tsx
// ❌ v3 — manual withLayoutContext wrapper
import { withLayoutContext } from 'expo-router';
import {
createTrueSheetNavigator,
type TrueSheetNavigationEventMap,
type TrueSheetNavigationOptions,
type TrueSheetNavigationState,
} from '@lodev09/react-native-true-sheet/navigation';
type ParamListBase = Record;
const { Navigator } = createTrueSheetNavigator();
const Sheet = withLayoutContext<
TrueSheetNavigationOptions,
typeof Navigator,
TrueSheetNavigationState,
TrueSheetNavigationEventMap
>(Navigator);
// ✅ v4 — import the Sheet layout directly
import { Sheet } from '@lodev09/react-native-true-sheet/navigation/expo-router';
```
In sheet screens, import `useTrueSheetNavigation` from the same entry point:
```tsx
// ❌ v3
import { useTrueSheetNavigation } from '@lodev09/react-native-true-sheet/navigation';
// ✅ v4 — Expo Router apps only
import { useTrueSheetNavigation } from '@lodev09/react-native-true-sheet/navigation/expo-router';
```
See the [Navigation guide](guides/navigation#expo-router) for more details.
### 9. Background Colors and Blur [#9-background-colors-and-blur]
On supported iOS 26.1+ devices, `backgroundColor` uses a native color effect and `backgroundBlur` uses a native blur effect.
On iOS, blur now takes precedence over color. Use `backgroundStyle.backgroundColor` to tint the blur:
```tsx
// ❌ v3 — color and blur blend
// ✅ v4 — tint the blur through the background wrapper
```
For tinted Liquid Glass, use `backgroundStyle` without `backgroundColor` or `backgroundBlur`:
```tsx
```
For an exact opaque color, including over full-screen modals, use `backgroundStyle.backgroundColor`:
```tsx
```
The native color effect can shift dark, low-chroma colors over full-screen modals.
Earlier iOS versions and devices without the background effect setters use a painted color or a blur view.
Design compatibility mode also uses this fallback on iOS 26. iOS 27+ ignores `UIDesignRequiresCompatibility`.
On iOS 26.0, the fallback sits above Liquid Glass. See [Background effects](guides/backgrounds#ios-background-effects) for details.
### 10. `anchor` Renamed to `placement` [#10-anchor-renamed-to-placement]
`anchor` is now `placement` and `anchorOffset` is now `placementOffset`. The values `'left'` and `'right'` are renamed to `'leading'` and `'trailing'` — native side sheets already followed the layout direction, so the new names match what the sheet does. Web now follows the layout direction too.
The default is the new `'automatic'` value, which lets the system decide. `'center'` is still available as an explicit request. The two only differ on iOS 27+, where they map to `UISheetPresentationController`'s `preferredPlacement`. Everywhere else they both center the sheet, so the default behavior is unchanged.
```tsx
// ❌ v3
// ✅ v4
```
See the [Side Sheets guide](guides/side-sheets) for more details.
### 11. `blurOptions` Removed [#11-bluroptions-removed]
Remove `blurOptions` and the `BlurOptions` type. The built-in blur now uses system intensity and ignores touches.
The `interaction: false` workaround for the iOS 18 touch flash is no longer needed.
For custom intensity, use a third-party blur in `background`:
```tsx
// ❌ v3
// ✅ v4 — with expo-blur installed
import { BlurView } from 'expo-blur';
import { StyleSheet } from 'react-native';
}
/>
```
The blur library controls platform support. See [Custom blur](guides/backgrounds#custom-blur).
## New Features in v4 [#new-features-in-v4]
### 1. `auto` Detent with Scrollables [#1-auto-detent-with-scrollables]
The `auto` detent now works with plugged scrollables — the sheet sizes to the scroll view's content height and resizes as content grows or shrinks, while the viewport is automatically bounded to the sheet's visible height. See the [Scrolling guide](guides/scrolling).
### 2. Floating Header with `headerOptions` [#2-floating-header-with-headeroptions]
The new `headerOptions` prop mirrors `footerOptions` — set `position` to `'absolute'` to float the header over the content, pinned to the top edge and excluded from the `auto` detent calculation. See the [Header guide](guides/header).
```tsx
} headerOptions={{ position: 'absolute' }}>
{/* content */}
```
### 3. Synchronous Per-Detent Layout [#3-synchronous-per-detent-layout]
The container is sized to the sheet's visible height per detent and tracks it in realtime while dragging, on all platforms. Flex layouts (e.g. a bottom-pinned button) follow the sheet's edge frame-by-frame instead of being sized to the largest detent.
### 4. First-Class Expo Router Support [#4-first-class-expo-router-support]
The new `/navigation/expo-router` entry point exports a ready-to-use `Sheet` layout that integrates with Expo Router's built-in navigation directly. See the [Navigation guide](guides/navigation#expo-router).
### 5. Static API with `createTrueSheetScreen` [#5-static-api-with-createtruesheetscreen]
The navigator now supports React Navigation's [static API](https://reactnavigation.org/docs/static-configuration):
```tsx
const Sheet = createTrueSheetNavigator({
screens: {
Main: MainScreen,
Details: createTrueSheetScreen({
screen: DetailsSheet,
options: { detents: ['auto', 1] },
}),
},
});
```
### 6. Element Inspector Support [#6-element-inspector-support]
React Native's element inspector now works inside a presented sheet. Open the dev menu, toggle the inspector, and tap any element in the sheet's header, content, or footer — the overlay and panel render inside the sheet, the same way they do inside a `Modal`. Dev builds only, no configuration needed.
### 7. Keep Absolute Footers Behind the Keyboard [#7-keep-absolute-footers-behind-the-keyboard]
On iOS and Android, the new `footerOptions.avoidKeyboard` option defaults to `true`, so absolute footers still rise above the keyboard. Set it to `false` to keep an absolute footer at the bottom edge of the sheet:
```tsx
} footerOptions={{ position: 'absolute', avoidKeyboard: false }}>
{/* content */}
```
With footer inset adjustment enabled, only the footer portion above the keyboard adds scroll padding and contributes to the expanded `auto` height. The pinned footer keeps its safe-area padding and ignores `keyboardOffset`. Relative footers are unchanged. See [Keyboard Handling](guides/keyboard#footer-behavior).
### 8. Overlays with `TrueSheetOverlay` [#8-overlays-with-truesheetoverlay]
The new `TrueSheetOverlay` component renders its children in a native layer above every presented sheet, on all platforms. It replaces the v3 `FullWindowOverlay` / `Modal` workaround for toasts and dialogs. Show or hide it by conditionally rendering children — there is no `visible` prop.
```tsx
// ❌ v3 — platform-specific workaround
const Overlay = Platform.select({
ios: FullWindowOverlay,
default: Modal,
})
// ✅ v4
import { TrueSheetOverlay } from '@lodev09/react-native-true-sheet'
{visible &&
```
See the [Overlays guide](guides/overlays) for touch handling, layout, and limitations.
### 9. Custom Backgrounds [#9-custom-backgrounds]
The new `background` and `backgroundStyle` props work on iOS, Android, and Web.
The background wrapper fills the sheet behind the header, content, and footer. It sits above the sheet effects and ignores touches.
It does not affect content height or detent calculations.
Use `backgroundStyle.backgroundColor` for an exact opaque color or a translucent tint over glass or blur:
```tsx
// Exact color, including over full-screen modals
// Tint the blur
```
Only `backgroundColor` is supported in `backgroundStyle`. It works without a `background` element and applies across all detents.
For a custom background, pass a `ReactNode` to `background`. Size the element with `StyleSheet.absoluteFill`.
See the [configuration reference](reference/configuration#background) and the [Backgrounds guide](guides/backgrounds).
### 10. Per-Detent Backgrounds [#10-per-detent-backgrounds]
The new `detentBackgrounds` prop replaces JavaScript background changes in `onDetentChange`:
```tsx
```
Supported iOS 26.1+ devices transition natively. Other devices cross-fade after passing the midpoint between detents.
Android and Web inherit the sheet background for blur entries.
See [`detentBackgrounds`](reference/configuration#detentbackgrounds).
### 11. Swipe-to-Dismiss Threshold [#11-swipe-to-dismiss-threshold]
On Android, a quick downward flick now dismisses the sheet from its lowest detent, like on iOS. A slow drag still has to pass half of the lowest detent.
Set the new `dismissThreshold` prop to `'short'` to dismiss on a short drag or any downward fling instead, like Compose Material3's `ModalBottomSheet`:
```tsx
{/* content */}
```
Works on Android and Web. iOS always uses the system behavior. See [`dismissThreshold`](reference/configuration#dismissthreshold).
### 12. Dim Color and Opacity [#12-dim-color-and-opacity]
The new `dimColor` and `dimOpacity` props change the color and strength of the dim behind the sheet:
```tsx
{/* content */}
```
On iOS, they replace the system dim with a custom dim layer. See the [Dimming guide](guides/dimming#dim-color-and-opacity).
## Step-by-Step Migration [#step-by-step-migration]
1. **Update React Native** to 0.82 or newer.
2. **Update the package:**
npm
pnpm
yarn
bun
```bash
npm install @lodev09/react-native-true-sheet@^4.0.0
```
```bash
pnpm add @lodev09/react-native-true-sheet@^4.0.0
```
```bash
yarn add @lodev09/react-native-true-sheet@^4.0.0
```
```bash
bun add @lodev09/react-native-true-sheet@^4.0.0
```
3. **Clean and reinstall iOS dependencies:**
```sh
cd ios
rm -rf Pods Podfile.lock build
pod install
cd ..
```
4. **Clean Android build:**
```sh
cd android
./gradlew clean
cd ..
```
5. **Update your code:**
* Replace the `scrollable` prop with `scrollableRef`, passing a ref of your scroll view
* Add `style={{ flex: 1 }}` where content should fill the sheet (bounded scroll views, spacer layouts)
* Add `footerOptions={{ position: 'absolute' }}` to keep a floating footer
* Remove manual safe-area padding from footers and scroll content — both are handled natively
* Use `backgroundStyle.backgroundColor` for glass/blur tints and exact opaque colors
* Remove `blurOptions`; use a third-party blur in `background` for custom intensity
* Rename `anchor` to `placement` and `anchorOffset` to `placementOffset`, with `"left"` / `"right"` becoming `"leading"` / `"trailing"`
* Replace `FullWindowOverlay` / `Modal` overlay workarounds with `TrueSheetOverlay`
* If using the Sheet Navigator, install `standard-navigation` and update `@react-navigation/native` to 7.3+
* If using Expo Router, install `standard-navigation`, replace the `withLayoutContext` wrapper with the `Sheet` layout from `/navigation/expo-router`, and update `useTrueSheetNavigation` imports
6. **Test your app:**
```sh
npx react-native run-ios
npx react-native run-android
```
## Need Help? [#need-help]
If you encounter any issues during migration:
* Check the [Troubleshooting](troubleshooting) guide
* Open an issue on [GitHub](https://github.com/lodev09/react-native-true-sheet/issues)
* Review the [example app](https://github.com/lodev09/react-native-true-sheet/tree/main/example) for reference implementations
# Troubleshooting (/next/troubleshooting)
## Blank Screen When Dismissing React Native Modal on iOS [#blank-screen-when-dismissing-react-native-modal-on-ios]
When presenting a TrueSheet on top of a React Native `` and then dismissing the Modal (by setting `visible={false}`), the screen may go blank. This is a bug in React Native where dismissing a Modal that has another view controller presented on top of it only dismisses the topmost view controller, leaving the Modal in an inconsistent state.
**Workaround:**
Dismiss the sheet first before hiding the Modal:
```tsx
const sheet = useRef(null)
const [modalVisible, setModalVisible] = useState(false)
const closeModal = async () => {
await sheet.current?.dismiss()
setModalVisible(false)
}
```
A fix has been submitted to React Native. See [PR #55005](https://github.com/facebook/react-native/pull/55005).
## Sheet Snaps Without Animation on Keyboard Dismiss (iOS) [#sheet-snaps-without-animation-on-keyboard-dismiss-ios]
On React Native 0.83 through 0.85, the sheet snaps back to its detent without animation when the keyboard is dismissed. This is caused by an upstream change in RN 0.83 where `canBecomeFirstResponder` returns `YES` on every `RCTViewComponentView`, disrupting `UISheetPresentationController`'s keyboard avoidance animation.
**Workaround:**
Apply a [patch](https://github.com/lodev09/react-native-true-sheet/blob/main/.yarn/patches/react-native-npm-0.85.3.patch) to `react-native` that gates `canBecomeFirstResponder` behind the `enableImperativeFocus` feature flag. For Yarn, add to your `package.json`:
```json
"resolutions": {
"react-native": "patch:react-native@npm%3A0.85.3#./.yarn/patches/react-native-npm-0.85.3.patch"
}
```
Fixed upstream in React Native 0.86. The patch is only needed on RN 0.83–0.85; remove it once you upgrade to 0.86+.
## Gesture Handler Not Working on Android [#gesture-handler-not-working-on-android]
If you're using `react-native-gesture-handler` inside the sheet and gestures are not working on Android, you need to wrap your sheet content with `GestureHandlerRootView`.
```tsx
import { GestureHandlerRootView } from 'react-native-gesture-handler'
{/* Your content with gestures */}
```
Use `flexGrow: 1` instead of `flex: 1` — the sheet's content wraps its natural height by default, so a `flex: 1` child collapses to zero height. Alternatively, pass `flex: 1` via the sheet's [`style`](reference/configuration#style) prop to fill the sheet. See [Content Layout](usage#content-layout).
[Learn more](https://docs.swmansion.com/react-native-gesture-handler/docs/fundamentals/installation#android) about gesture handler setup on Android.
## Initial Present and Deep Linking [#initial-present-and-deep-linking]
On iOS, a deep link on a cold start can open another screen over the one hosting an auto-presenting sheet — a modal, or a sheet in a [sheet navigator](guides/navigation). The `initialDetentIndex` sheet then lands on top of it or doesn't present at all.
Hold `initialDetentIndex` until the screen is focused. See [Deep Linking](guides/deep-linking) for the pattern and a runnable example.
## Large Title Doesn't Collapse on Scroll (iOS) [#large-title-doesnt-collapse-on-scroll-ios]
With a native stack's `headerLargeTitleEnabled`, iOS tracks the screen's scroll view through its first child. A `TrueSheet` rendered before the scroll view takes that spot, so the large title stays expanded.
Render the sheet after the scroll view:
```tsx
<>
{/* ... */}{/* ... */}
>
```
## Weird layout render [#weird-layout-render]
The sheet's content lays out naturally — like a regular view or a screen — wrapping its children's natural height. Common symptoms and fixes:
* **`flex: 1` children render with zero height** — the content isn't filled by default. Pass `flex: 1` via the sheet's [`style`](reference/configuration#style) prop so the content fills the sheet's visible height. See [Content Layout](usage#content-layout).
* **Content taller than the sheet gets clipped** — use a `ScrollView` with `flex: 1` on the sheet's `style` (see [Scrolling Content](guides/scrolling)) or size your layout with `flexGrow`, `flexBasis`, or fixed heights.
If layout still misbehaves, try moving the sheet component up to a higher level in the component tree as some container may interfere with the sheet's content rendering.
## `@expo/ui` Host Opens Small, Then Grows [#expoui-host-opens-small-then-grows]
With an `auto` detent, a sheet containing an [`@expo/ui`](https://docs.expo.dev/versions/latest/sdk/ui/) `` opens at a small height, then grows to fit the content.
Jetpack Compose and SwiftUI content can only measure once it's attached to a window, and sheet content is only attached on `present()`. The `Host` reports its size a few frames into the present animation, and the sheet resizes to it. [`lazy={false}`](reference/configuration#lazy) doesn't help here — it mounts the content earlier, but doesn't attach it.
To open at the right height, give the `Host` a known height instead of `matchContents`:
```tsx
const [height, setHeight] = useState(480);
nativeEvent.height > 0 && setHeight(nativeEvent.height)}
>
```
## Xcode and EAS Build Issues [#xcode-and-eas-build-issues]
### Xcode Version [#xcode-version]
The package requires **Xcode 26.1 or later**. Background functionality may not work correctly with older versions. Check your Xcode version:
```sh
xcodebuild -version
```
### EAS Build Configuration [#eas-build-configuration]
When using [EAS Build](https://docs.expo.dev/build/introduction/), configure your `eas.json` to use a build image with Xcode 26.1+. Use `"image": "latest"` or choose from the [available build images](https://docs.expo.dev/build-reference/infrastructure/#ios-server-images):
```json
{
"build": {
"production": {
"ios": {
"image": "latest"
}
},
"development": {
"ios": {
"image": "latest"
}
}
}
}
```
### Patched `react-native-screens` Not Applied on EAS (Precompiled Modules) [#patched-react-native-screens-not-applied-on-eas-precompiled-modules]
If you apply the [`react-native-screens` patch](guides/navigation) so the sheet stays visible when presenting screens on top, you may find that on **Expo SDK 56+ EAS builds** the sheet dismisses — or a full-screen modal fails to present over the sheet — even though it works in a local build.
This is because Expo SDK 56 ships `react-native-screens` (and other common libraries like `react-native-reanimated`, `react-native-svg`) as **precompiled XCFrameworks** on EAS. The prebuilt framework is built from the library's own source, so any patch you apply to `react-native-screens` (via `patch-package`, `pnpm patch`, or Yarn `patches`) is **never compiled in** — it's silently ignored on EAS, while local builds still work because they fall back to compiling from source.
**Fix:**
Disable precompiled modules so `react-native-screens` builds from your patched source. Add the env var to each build profile in `eas.json`:
```json
{
"build": {
"development": { "env": { "EXPO_USE_PRECOMPILED_MODULES": "0" } },
"preview": { "env": { "EXPO_USE_PRECOMPILED_MODULES": "0" } },
"production": { "env": { "EXPO_USE_PRECOMPILED_MODULES": "0" } }
}
}
```
This only disables **module** precompilation — React Native core stays prebuilt, so build times are largely unaffected.
To keep precompiled modules for everything else and only build `react-native-screens` from source, opt it out per-package in `package.json` instead:
```json
{
"expo": {
"autolinking": {
"ios": { "buildFromSource": ["react-native-screens"] }
}
}
}
```
Avoid `ios.buildReactNativeFromSource: true` for this — it also rebuilds React Native core from source, which is much slower.
# Usage (/next/usage)
1. Import `TrueSheet`.
```tsx
import { TrueSheet } from "@lodev09/react-native-true-sheet"
```
2. Define `TrueSheet` inside any component and attach a `ref` to it.
3. Control the sheet by invoking the available `ref` [methods](reference/methods).
```tsx {2-2,5-8,11-14,18-25}
export const App = () => {
const sheet = useRef(null)
// Present the sheet ✅
const present = async () => {
await sheet.current?.present()
console.log('horray! sheet has been presented 💩')
}
// Dismiss the sheet ✅
const dismiss = async () => {
await sheet.current?.dismiss()
console.log('Bye bye 👋')
}
return (
)
}
```
Simple, right? Head over to [Configuration](reference/configuration) to learn more about configuring your sheet.
## Content Layout [#content-layout]
Content lays out naturally — just like a regular view or a screen in [React Navigation](https://reactnavigation.org). By default, the sheet's content wraps its children's natural height. To fill the sheet's visible height (e.g. for `flex: 1` children, centered content, or `justifyContent` layouts), pass `flex: 1` via the [`style`](reference/configuration#style) prop:
```tsx
{/* Fills the sheet at every detent */}
```
Scroll views work the same way — bound them with `flex: 1` like above. They're only special-cased for keyboard handling and [`auto`](reference/types#sheetdetent) sizing, where the sheet derives its height from the scroll content. See [Scrolling Content](guides/scrolling).
## Skills [#skills]
Skills are reusable AI capabilities that give your AI coding agent knowledge about TrueSheet. With the right skill loaded, your agent can pick the right patterns, avoid common mistakes, and generate correct code without you having to explain the library every time.
```sh
npx skills add lodev09/react-native-true-sheet
```
This will install the **TrueSheet Usage** skill into your project.
# Dimming the Background (/guides/dimming)
One of the most common use cases for a Bottom Sheet is to present it while still allowing users to interact with background components, such as in a Maps app.
In this guide, you can configure `TrueSheet` to achieve this exact functionality.
## How? [#how]
You can easily disable the dimmed background of the sheet by setting [`dimmed`](../reference/configuration#dimmed) to `false`.
```tsx {5}
export const App = () => {
return (
)
}
```
### Dimmed by Detent Index [#dimmed-by-detent-index]
To further customize the dimming behavior, [`dimmedDetentIndex`](../reference/configuration#dimmeddetentindex) is also available. Set the [detent](../reference/configuration#detents) `index` at which you want the sheet to start dimming.
```tsx {5}
export const App = () => {
return (
)
}
```
`dimmedDetentIndex` is ignored if `dimmed` is set to `false`.
## Customizing Dimming Alpha [#customizing-dimming-alpha]
You can dynamically control the dimming opacity based on the sheet's position using `ReanimatedTrueSheet` with `animatedPosition`.
Learn more about Reanimated integration in the [Reanimated guide](reanimated).
```tsx {2-3,7,9-11,14-21}
import { ReanimatedTrueSheet, ReanimatedTrueSheetProvider, useReanimatedTrueSheet } from '@lodev09/react-native-true-sheet/reanimated'
import Animated, { useAnimatedStyle } from 'react-native-reanimated'
import { StyleSheet } from 'react-native'
export const App = () => {
return (
)
}
const CustomBackdrop = () => {
const { animatedPosition } = useReanimatedTrueSheet()
const backdropStyle = useAnimatedStyle(() => ({
opacity: animatedPosition.value * 0.5, // Adjust multiplier for desired alpha
}))
return
}
const Sheet = () => {
return (
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
backdrop: {
backgroundColor: 'black',
},
})
```
This allows you to create custom dimming effects that respond to the sheet's movement in real-time.
# Edge-to-Edge (/guides/edge-to-edge)
TrueSheet automatically detects and adapts to Android's edge-to-edge display mode, providing a modern, immersive experience.
## Enabling Edge-to-Edge [#enabling-edge-to-edge]
Edge-to-edge is supported in React Native 0.81+ via the `edgeToEdgeEnabled` Gradle property.
Enable it in your `android/gradle.properties`:
```groovy title="android/gradle.properties"
edgeToEdgeEnabled=true
```
This property is **disabled by default** for current Android versions. TrueSheet will auto-detect when you enable it.
Starting with **Android 16+ (API level 36)**, edge-to-edge will be [automatically enabled by default](https://developer.android.com/about/versions/16/behavior-changes-16#edge-to-edge) by the system.
**Learn more:**
* [React Native 0.81 edge-to-edge announcement](https://reactnative.dev/blog/2025/08/12/react-native-0.81)
* [Android 16 edge-to-edge enforcement](https://developer.android.com/about/versions/16/behavior-changes-16#edge-to-edge)
## Default Behavior [#default-behavior]
By default, when edge-to-edge is enabled, the sheet respects the status bar and stops at the bottom of it when fully expanded. This ensures content remains visible and not obscured by the status bar.
```tsx
```
TrueSheet works seamlessly with edge-to-edge enabled and provides automatic status bar detection.
# Adding Footer (/guides/footer)
Do you need a FAB (Floating Action Button) on the sheet? Or perhaps a small banner at the bottom? We've got you covered!
`TrueSheet` provides first-class support for a `Footer` component. This component will float at the bottom of the sheet.
## How? [#how]
Define your footer component.
```tsx
const SomeFooter = () => {
return (
My Foot-er is Awesome.
)
}
```
Stick it in the [`footer`](../reference/configuration#footer).
```tsx {3}
const App = () => {
return (
)
}
```
For best performance, use `ReactElement` instead!
```tsx {5-9}
const App = () => {
return (
My Foot-er is more awesome.
}
>
)
}
```
## Safe Area Handling [#safe-area-handling]
The footer is pinned to the bottom edge of the sheet. The sheet height automatically includes the bottom safe area on both iOS and Android. To ensure your footer extends properly into the safe area, add bottom padding:
```tsx
import { Platform } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
const isIPad = Platform.OS === 'ios' && Platform.isPad;
const MyFooter = () => {
const insets = useSafeAreaInsets();
const bottomInset = isIPad ? 0 : insets.bottom;
return (
Footer Content
);
};
// Usage
}>
{/* content */}
```
This ensures the footer background extends into the safe area while keeping the content above the home indicator.
When the keyboard opens, that bottom padding leaves a gap above the keyboard. Pass `-insets.bottom` to [`footerOptions.keyboardOffset`](../reference/configuration#footeroptions) to tuck it behind the keyboard — see [Keyboard Handling](./keyboard#footer-behavior).
On iPad, the sheet is displayed as a floating modal, so bottom padding is not needed.
## Collapsing to the Footer [#collapsing-to-the-footer]
The footer height counts toward the [`"peek"`](../reference/types#sheetdetent) detent — collapse the sheet down to just the [`header`](header), footer, and any peeking content.
```tsx {4-4}
const App = () => {
return (
}
>
)
}
```
Since the footer floats, it stays visible at every detent. See [Peeking Content](peeking) for the full guide.
# Global Methods (/guides/global-methods)
`TrueSheet` relies heavily on [component methods](../reference/methods) to present and dismiss the sheet. However, this becomes challenging when the button is deep within the component tree.
To resolve this issue, `TrueSheet` provides [global methods](../reference/methods#global-methods) that allow you to present or dismiss any sheet from anywhere!
## How? [#how]
Somewhere in your App, define the sheet with a [`name`](../reference/configuration#name).
```tsx {3}
const App = () => {
return (
)
}
```
In your component, present the sheet using the `present` global method.
```tsx {5}
import { TrueSheet } from '@lodev09/react-native-true-sheet'
const SomeComponent = () => {
const presentMySheet = () => {
TrueSheet.present('my-sheet') // 🎉
}
return (
)
}
```
Guess what? You can also `dismiss` it globally 🤩.
```tsx
TrueSheet.dismiss('my-sheet') // ✅
```
# Adding Header (/guides/header)
Need a fixed header above your scrollable content? A title bar, search input, or navigation controls? TrueSheet makes it simple with the `header` prop!
## Using the `header` Prop [#using-the-header-prop]
The recommended way to add a header is using the `header` prop. This creates a native header view that is properly accounted for in layout calculations, ensuring your scrollable content gets the correct available height.
```tsx {4-8}
const App = () => {
return (
My Header
}
>
{/* Your scrollable content */}
)
}
const styles = StyleSheet.create({
header: {
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
})
```
## With Search Input [#with-search-input]
A common use case is adding a search bar in the header:
```tsx {4-8}
const App = () => {
return (
}
>
}
/>
)
}
```
When using `header` with `FlatList` or `ScrollView`, the content area height is automatically adjusted to account for the header height. This ensures proper scrolling behavior on both iOS and Android.
## Collapsing to the Header [#collapsing-to-the-header]
Use the [`"peek"`](../reference/types#sheetdetent) detent to collapse the sheet down to just the header (plus [`footer`](footer), if provided). Great for map-style sheets with a compact summary that expands into full content.
```tsx {4-4}
const App = () => {
return (
}
footer={}
scrollable
>
)
}
```
The peek height automatically follows the measured header and footer sizes — no `onLayout` juggling needed. You can also include part of the content via the `TrueSheetPeek` component — see [Peeking Content](peeking) for the full guide.
## Platform Support [#platform-support]
The `header` prop is supported on both **iOS** and **Android**.
# Testing with Jest (/guides/jest)
Testing components that use `TrueSheet` is straightforward with the built-in Jest mocks.
## Setup [#setup]
Add the mocks to your Jest setup file:
```js
// jest.setup.js
// Main component
jest.mock('@lodev09/react-native-true-sheet', () =>
require('@lodev09/react-native-true-sheet/mock')
);
// Navigation (if using)
jest.mock('@lodev09/react-native-true-sheet/navigation', () =>
require('@lodev09/react-native-true-sheet/navigation/mock')
);
// Reanimated (if using)
jest.mock('@lodev09/react-native-true-sheet/reanimated', () =>
require('@lodev09/react-native-true-sheet/reanimated/mock')
);
```
Configure Jest to use the setup file and transform the package:
```json
{
"jest": {
"setupFilesAfterEnv": ["/jest.setup.js"],
"transformIgnorePatterns": [
"node_modules/(?!(react-native|@react-native|@lodev09/react-native-true-sheet)/)"
]
}
}
```
The `transformIgnorePatterns` config is required because the mock files use ESM syntax. This tells Jest to transform the package instead of skipping it.
## Available Mocks [#available-mocks]
### Main Module (`/mock`) [#main-module-mock]
* `TrueSheet` - Component with mocked `present`, `dismiss`, `resize` methods
* `TrueSheetProvider` - Pass-through provider
* `useTrueSheet` - Hook returning mocked methods
### Navigation Module (`/navigation/mock`) [#navigation-module-navigationmock]
* `createTrueSheetNavigator` - Mocked navigator factory
* `TrueSheetActions` - Mocked action creators
* `useTrueSheetNavigation` - Hook returning mocked navigation object
### Reanimated Module (`/reanimated/mock`) [#reanimated-module-reanimatedmock]
* `ReanimatedTrueSheet` - Component with mocked methods
* `ReanimatedTrueSheetProvider` - Pass-through provider
* `useReanimatedTrueSheet` - Hook returning mocked shared values
* `useReanimatedPositionChangeHandler` - Mocked handler hook
## Testing Static Methods [#testing-static-methods]
All static methods are mocked as Jest functions.
```tsx
import { TrueSheet } from '@lodev09/react-native-true-sheet';
it('should present sheet', async () => {
await TrueSheet.present('my-sheet', 0);
expect(TrueSheet.present).toHaveBeenCalledWith('my-sheet', 0);
});
it('should dismiss sheet', async () => {
await TrueSheet.dismiss('my-sheet');
expect(TrueSheet.dismiss).toHaveBeenCalledWith('my-sheet');
});
```
## Testing Component Rendering [#testing-component-rendering]
The mock renders `TrueSheet` as a View with all props passed through.
```tsx
it('should render sheet content', () => {
const { getByText } = render(
Sheet Content
);
expect(getByText('Sheet Content')).toBeDefined();
});
```
## Testing Reanimated Integration [#testing-reanimated-integration]
```tsx
import {
useReanimatedTrueSheet,
ReanimatedTrueSheetProvider,
} from '@lodev09/react-native-true-sheet/reanimated';
it('should return mocked shared values', () => {
const { result } = renderHook(() => useReanimatedTrueSheet(), {
wrapper: ReanimatedTrueSheetProvider,
});
expect(result.current.animatedPosition.value).toBe(0);
expect(result.current.animatedIndex.value).toBe(-1);
});
```
## Best Practices [#best-practices]
Clear mock calls between tests to avoid interference.
```tsx
describe('MyComponent', () => {
beforeEach(() => {
jest.clearAllMocks();
});
// Your tests...
});
```
All methods return resolved Promises, so remember to `await` them in your tests.
# Keyboard Handling (/guides/keyboard)
TrueSheet handles keyboard visibility natively on both iOS and Android. When a `TextInput` inside the sheet is focused, the sheet automatically adjusts to keep the input visible above the keyboard.
## How? [#how]
The keyboard handling is built into the native implementation:
* **iOS**: Uses `UISheetPresentationController`'s built-in keyboard avoidance
* **Android**: Tracks keyboard height via `WindowInsetsAnimationCompat` and reconfigures sheet detents in real-time
No additional configuration is required. Simply add your `TextInput` components inside the sheet:
```tsx
const App = () => {
return (
)
}
```
## Scrollable + Keyboard [#scrollable--keyboard]
When using [`scrollable`](../reference/configuration#scrollable), keyboard handling works out of the box. If a `TextInput` inside a `ScrollView` is focused, the sheet automatically scrolls to keep the input visible above the keyboard — no extra setup needed.
You can use `keyboardScrollOffset` in [`scrollableOptions`](../reference/configuration#scrollableoptions) to add extra spacing between the focused input and the keyboard:
```tsx
```
This is especially useful for forms with multiple inputs — as the user taps between fields, the scroll view adjusts automatically to keep the active input in view.
## Footer Behavior [#footer-behavior]
When using a [`footer`](../reference/configuration#footer) component, it automatically repositions above the keyboard, staying visible while the user types.
If your footer renders its own bottom inset (e.g. safe-area padding for the home indicator), that padding leaves a gap above the keyboard. Use `keyboardOffset` in [`footerOptions`](../reference/configuration#footeroptions) to adjust the rise — pass `-insets.bottom` to tuck the inset behind the keyboard:
```tsx
const insets = useSafeAreaInsets()
} footerOptions={{ keyboardOffset: -insets.bottom }}>
{/* ... */}
```
The footer slides up by the keyboard height plus `keyboardOffset`. A negative value reduces the rise so the inset sits behind the keyboard; a positive value raises the footer higher.
## Autofocus Limitation [#autofocus-limitation]
Avoid using `autoFocus` on `TextInput` components inside the sheet. The keyboard may appear before the sheet has finished presenting, causing layout issues.
Instead, focus the input programmatically after the sheet has presented:
```tsx {4,7}
const inputRef = useRef(null)
const handlePresent = () => {
inputRef.current?.focus()
}
```
# Liquid Glass (/guides/liquid-glass)
Starting with iOS 26, Apple introduced the **Liquid Glass** visual effect, a new design element that creates a frosted glass appearance on sheets and modals.
TrueSheet **supports Liquid Glass by default** on iOS 26+, giving your sheets a modern, native look.
## What is Liquid Glass? [#what-is-liquid-glass]
Liquid Glass is a visual effect introduced in iOS 26 that provides a translucent, frosted glass appearance with a subtle blur. It's part of Apple's latest design language and is automatically applied to native sheet presentations.
By default, TrueSheet enables Liquid Glass on iOS 26+ devices when no [`backgroundColor`](../reference/configuration#backgroundcolor) or [`backgroundBlur`](../reference/configuration#backgroundblur) is provided. The sheet will automatically display with the Liquid Glass effect.
## Disabling Liquid Glass [#disabling-liquid-glass]
If you prefer the classic sheet appearance without Liquid Glass, there are two options:
### Using `backgroundColor` or `backgroundBlur` [#using-backgroundcolor-or-backgroundblur]
Setting [`backgroundColor`](../reference/configuration#backgroundcolor) or [`backgroundBlur`](../reference/configuration#backgroundblur) (or both) on the sheet will disable the Liquid Glass effect for that specific sheet.
```tsx
{/* Sheet content */}
```
```tsx
{/* Sheet content */}
```
This approach allows you to disable Liquid Glass on a per-sheet basis while keeping it enabled for other sheets in your app.
This only works on iOS 26.1 and above.
### Using `UIDesignRequiresCompatibility` [#using-uidesignrequirescompatibility]
On iOS 26, set `UIDesignRequiresCompatibility` to `true` in your `Info.plist` to disable Liquid Glass.
iOS 27 and later ignore this key. Use `backgroundColor` or `backgroundBlur` to replace Liquid Glass on individual sheets.
#### Using Info.plist [#using-infoplist]
Add the following key to your `ios/YourApp/Info.plist`:
```xml title="ios/YourApp/Info.plist"
UIDesignRequiresCompatibility
```
#### Using Expo Config Plugin [#using-expo-config-plugin]
If you're using Expo, you can configure this through your `app.json` or `app.config.js`:
```js title="app.config.js"
export default {
expo: {
ios: {
infoPlist: {
UIDesignRequiresCompatibility: true,
},
},
},
}
```
After making this change, rebuild your app:
```sh
npx expo prebuild --clean
npx expo run:ios
```
On iOS 26, this setting disables Liquid Glass across the entire app. It has no effect on iOS 27+.
See [Apple's documentation](https://developer.apple.com/documentation/BundleResources/Information-Property-List/UIDesignRequiresCompatibility) for more details.
## Learn More [#learn-more]
* [How to create Apple Maps style liquid glass sheets in Expo](https://expo.dev/blog/how-to-create-apple-maps-style-liquid-glass-sheets) - Expo Blog
* [Apple's Liquid Glass Documentation](https://developer.apple.com/documentation/TechnologyOverviews/liquid-glass)
# React Navigation (/guides/navigation)
TrueSheet integrates with React Navigation out of the box. It just works!
## How? [#how]
You can use the [Sheet Navigator](#sheet-navigator) to present screens as sheets (also works with [Expo Router](#expo-router)), or simply [navigate from within sheets](#navigating-from-sheets) using your existing navigation setup.
## Sheet Navigator [#sheet-navigator]
TrueSheet provides a custom navigator for React Navigation. The first screen (or `initialRouteName`) is the base content, while other screens are presented as sheets.
npm
pnpm
yarn
bun
```bash
npm install @react-navigation/native
```
```bash
pnpm add @react-navigation/native
```
```bash
yarn add @react-navigation/native
```
```bash
bun add @react-navigation/native
```
### Basic Usage [#basic-usage]
```tsx
import { NavigationContainer } from '@react-navigation/native';
import {
createTrueSheetNavigator,
useTrueSheetNavigation,
} from '@lodev09/react-native-true-sheet/navigation';
const Sheet = createTrueSheetNavigator();
function App() {
return (
{/* Base screen (first screen is the default) */}
{/* Sheet screens */}
);
}
```
### Wrapping Existing Navigation [#wrapping-existing-navigation]
Wrap your root navigator to present sheets from anywhere:
```tsx
const Stack = createNativeStackNavigator();
const Sheet = createTrueSheetNavigator();
function RootStack() {
return (
);
}
function App() {
return (
);
}
```
### Navigation & Resizing [#navigation--resizing]
```tsx
function DetailsSheet() {
const navigation = useTrueSheetNavigation();
return (
);
}
```
### Screen Options [#screen-options]
All [TrueSheet props](../reference/configuration) are available as screen options, plus the following navigation-specific options:
| Option | Type | Description |
| ----------------------- | ---------- | --------------------------------------------------------------------------------------------------------------- |
| `detentIndex` | `number` | The detent index to present at. Defaults to `0`. |
| `reanimated` | `boolean` | Enable worklet-based position events for this screen. |
| `positionChangeHandler` | `function` | A callback that receives position change events. When `reanimated` is enabled, this must be a worklet function. |
### Reanimated Integration [#reanimated-integration]
Enable worklet-based position events for smooth UI thread animations:
```tsx
// In your navigator
{
'worklet';
// Access payload.position, payload.detentIndex, etc.
console.log(payload.position);
},
}}
/>
```
When `reanimated: true` is set, `react-native-reanimated` must be installed and `positionChangeHandler` must be a worklet function. The integration is lazy-loaded, so screens without `reanimated: true` don't require reanimated.
### Dynamic Header & Footer [#dynamic-header--footer]
Use [`navigation.setOptions()`](https://reactnavigation.org/docs/navigation-object/#setoptions) to set or update `header` and `footer` from within a sheet screen. This is useful when you need access to navigation state or sheet events.
```tsx
function DetailsSheet() {
const navigation = useTrueSheetNavigation();
const [detentIndex, setDetentIndex] = useState(0);
useEffect(() => {
const unsubscribe = navigation.addListener('sheetDetentChange', (e) => {
setDetentIndex(e.data.index);
});
return unsubscribe;
}, [navigation]);
useEffect(() => {
navigation.setOptions({
footer: (
{detentIndex > 0 && navigation.resize(0)} />}
navigation.goBack()} />
),
});
}, [navigation, detentIndex]);
return {/* ... */};
}
```
All [TrueSheet props](../reference/configuration) like `header`, `footer`, `grabber`, `dismissible`, etc. can be dynamically updated via `setOptions`.
### Screen Listeners [#screen-listeners]
Use `screenListeners` on the navigator or `listeners` on individual screens:
```tsx
console.log('Presented:', e.data.index),
sheetDidDismiss: () => console.log('Dismissed'),
}}
>
```
Or use `addListener` within a screen component:
```tsx
function DetailsSheet() {
const navigation = useTrueSheetNavigation();
useEffect(() => {
const unsubscribe = navigation.addListener('sheetDidPresent', (e) => {
console.log('Presented:', e.data.index);
});
return unsubscribe;
}, [navigation]);
return {/* ... */};
}
```
| Event | Description |
| --------------------- | ------------------------- |
| `sheetWillPresent` | Sheet is about to present |
| `sheetDidPresent` | Sheet finished presenting |
| `sheetWillDismiss` | Sheet is about to dismiss |
| `sheetDidDismiss` | Sheet finished dismissing |
| `sheetDetentChange` | Detent changed |
| `sheetDragBegin` | User started dragging |
| `sheetDragChange` | User is dragging |
| `sheetDragEnd` | User stopped dragging |
| `sheetPositionChange` | Position changed |
See [Lifecycle Events](../reference/events) for more details.
## Expo Router [#expo-router]
The [Sheet Navigator](#sheet-navigator) works with Expo Router using `withLayoutContext`. All [navigator features](#sheet-navigator) above (screen options, reanimated, dynamic header/footer, listeners) apply here too.
```
app/
├── _layout.tsx # TrueSheet navigator
├── index.tsx # Base content
└── details.tsx # Sheet screen
```
```tsx
// app/_layout.tsx
import { withLayoutContext } from 'expo-router';
import {
createTrueSheetNavigator,
type TrueSheetNavigationEventMap,
type TrueSheetNavigationOptions,
type TrueSheetNavigationState,
} from '@lodev09/react-native-true-sheet/navigation';
type ParamListBase = Record;
const { Navigator } = createTrueSheetNavigator();
const Sheet = withLayoutContext<
TrueSheetNavigationOptions,
typeof Navigator,
TrueSheetNavigationState,
TrueSheetNavigationEventMap
>(Navigator);
export default function SheetLayout() {
return (
);
}
```
See [Expo Router docs](https://docs.expo.dev/router/introduction/) for more information.
The Sheet Navigator works with Expo Router SDK 56+ without installing `@react-navigation/native`. Under SDK 56, Expo Router no longer allows app code to import directly from `@react-navigation/*` — import navigation types/utilities from `expo-router` (or `expo-router/react-navigation`) instead. See the [Expo Router SDK 55 → 56 migration guide](https://docs.expo.dev/router/migrate/sdk-55-to-56/).
## Navigating from Sheets [#navigating-from-sheets]
Navigate directly from sheets - they remain visible when presenting modals on top.
```tsx
// Navigate directly - no need to dismiss first!
navigation.navigate('SomeScreen')
```
Requires a [patch to react-native-screens](https://github.com/lodev09/react-native-true-sheet/blob/main/.yarn/patches/react-native-screens-npm-4.25.2.patch). See [PR #3415](https://github.com/software-mansion/react-native-screens/pull/3415).
On **Expo SDK 56+**, this patch is silently dropped on EAS builds because `react-native-screens` ships precompiled. See [Patched react-native-screens Not Applied on EAS](../troubleshooting#patched-react-native-screens-not-applied-on-eas-precompiled-modules) to force it to build from source.
### Web Limitation [#web-limitation]
On native platforms, TrueSheet automatically detects `react-native-screens` and handles sheet visibility when navigating. However, this detection is not supported on web.
As a workaround, use `useFocusEffect` to manually present/dismiss the sheet when the screen gains or loses focus:
```tsx
import { useFocusEffect } from '@react-navigation/native';
function DetailsSheet() {
const sheet = useRef(null);
useFocusEffect(
useCallback(() => {
sheet.current?.present();
return () => {
sheet.current?.dismiss();
};
}, [])
);
return {/* ... */};
}
```
# Present On Mount (/guides/onmount)
Sometimes, you may want to present the sheet directly during mount. For example, you might want to present the sheet when a screen is opened through a deep link.
## How? [#how]
You can do this by setting [`initialDetentIndex`](../reference/configuration#initialdetentindex) prop. It accepts the [`detent`](../reference/types#sheetdetent) `index` that your sheet is configured with. See [detents](../reference/configuration#detents) prop for more information.
```tsx {5-6}
const App = () => {
return (
)
}
```
### Disabling Animation [#disabling-animation]
You may want to disable the present animation. To do this, simply set [`initialDetentAnimated`](../reference/configuration#initialdetentanimated) to `false`.
### Deep Linking on iOS [#deep-linking-on-ios]
iOS Limitation
`initialDetentIndex` may not work when deep linking to a modal screen from a cold start. See [troubleshooting](../troubleshooting#initial-present-and-deep-linking) for workaround.
# Overlays on Sheets (/guides/overlays)
When displaying overlays like dialogs or modals on top of a sheet, you may encounter z-index issues where the sheet appears above the overlay, particularly on Android.
This guide explains how to properly display overlays on top of TrueSheet.
Portal-based UI libraries (like `react-native-reusables`, `tamagui`, etc.) render content outside the normal component hierarchy. On Android, the sheet may appear on top of these portals due to how native views are layered.
## How? [#how]
Wrap your overlay content with React Native's `Modal` on Android and `FullWindowOverlay` from `react-native-screens` on iOS.
```tsx
import { Platform, Modal } from 'react-native'
import { FullWindowOverlay } from 'react-native-screens'
const Overlay = Platform.select({
ios: FullWindowOverlay,
default: Modal,
})
export const App = () => {
const [visible, setVisible] = useState(false)
return (
<>
setVisible(true)} />
setVisible(false)} />
>
)
}
```
## Platform Differences [#platform-differences]
| Platform | Recommended Approach |
| -------- | ----------------------------------------------- |
| iOS | `FullWindowOverlay` from `react-native-screens` |
| Android | React Native `Modal` with `transparent` prop |
`FullWindowOverlay` requires `react-native-screens` to be installed.
## Why This Works [#why-this-works]
* **iOS**: `FullWindowOverlay` renders content in a separate window above all other views
* **Android**: `Modal` creates a new native dialog that appears above the sheet's `CoordinatorLayout`
Both approaches ensure the overlay is rendered in a separate native container that sits above the sheet in the view hierarchy.
# Peeking Content (/guides/peeking)
Building a map-style sheet with a compact summary that expands into full content? The [`"peek"`](../reference/types#sheetdetent) detent collapses the sheet down to just the essentials — no manual height juggling needed.
## The `"peek"` Detent [#the-peek-detent]
Add `"peek"` to your [`detents`](../reference/configuration#detents) to get a collapsed size based on the measured [`header`](header) and [`footer`](footer) heights.
```tsx {4-4}
const App = () => {
return (
}
footer={}
scrollable
>
)
}
```
The peek height automatically follows the measured header and footer sizes. When neither `header` nor `footer` is provided, it falls back to a fixed height of `150`.
## Peeking Part of the Content [#peeking-part-of-the-content]
To include part of the content in the peek height, wrap it with the `TrueSheetPeek` component.
```tsx {8-10}
import { TrueSheet, TrueSheetPeek } from '@lodev09/react-native-true-sheet'
const App = () => {
return (
)
}
```
When collapsed, the sheet reveals everything from the top of the sheet through the **bottom edge** of `TrueSheetPeek` — content padding and anything rendered above it count toward the height, while content below it stays hidden until the sheet is expanded.
```
┌──────────────────────┐
│ header │ ─┐
│ ┌──────────────────┐ │ │
│ │ TrueSheetPeek │ │ ├─ visible when collapsed
│ └──────────────────┘ │ │
│ footer │ ─┘
├──────────────────────┤
│ rest of the content │ ── hidden until expanded
└──────────────────────┘
```
`TrueSheetPeek` can be placed anywhere within the content — the collapsed sheet simply reveals everything above it, including itself. A sheet can only have **one** peek component.
Since everything above it counts, an **empty** `TrueSheetPeek` works as a fold marker — drop it between your existing views to mark where the collapsed sheet ends, no wrapping needed.
```tsx {3}
```
## Styling [#styling]
`TrueSheetPeek` is a regular `View` — style it like any other.
```tsx
True SheetThe true native bottom sheet experience.
```
Note that only the content through its bottom edge counts — a `marginBottom` on the peek itself is *not* included. If you want breathing room below the peek content while collapsed, use `paddingBottom` instead.
## Safe Area [#safe-area]
By default ([`insetAdjustment="automatic"`](../reference/configuration#insetadjustment)), the bottom safe area inset is added to the peek height so the peek content clears the system navigation bar. Set `insetAdjustment="never"` if you're handling insets yourself.
## Resizing [#resizing]
`"peek"` works like any other detent — resize to it programmatically or listen for changes. See [Resizing Programmatically](resizing).
```tsx
sheet.current?.resize(0) // collapse back to peek
```
## Platform Support [#platform-support]
The `"peek"` detent and `TrueSheetPeek` are supported on **iOS 16+**, **Android**, and **Web**.
# Reanimated (/guides/reanimated)
`TrueSheet` has first-class support for [react-native-reanimated v4](https://docs.swmansion.com/react-native-reanimated/).
Requirements
* `react-native-reanimated`: ^4.0.0
* `react-native-worklets` (peer dependency of Reanimated v4)
## How? [#how]
### 1. Add the Provider [#1-add-the-provider]
Manages shared values for Reanimated integration.
```tsx
import { ReanimatedTrueSheetProvider } from '@lodev09/react-native-true-sheet/reanimated'
function App() {
return (
)
}
```
### 2. Use ReanimatedTrueSheet [#2-use-reanimatedtruesheet]
Animated sheet component that syncs position automatically with all props from [`TrueSheet`](../reference/configuration).
```tsx
import { type TrueSheet } from '@lodev09/react-native-true-sheet'
import { ReanimatedTrueSheet } from '@lodev09/react-native-true-sheet/reanimated'
function MyScreen() {
const sheetRef = useRef(null)
return (
Sheet Content
)
}
```
Note that the `onPositionChange` prop event now runs on the UI thread (worklet). If you override this prop, make sure to add the `'worklet'` directive to your handler.
### 3. Access Animated Values [#3-access-animated-values]
Use the `useReanimatedTrueSheet` hook to access the sheet's animated values.
```tsx
import { useReanimatedTrueSheet } from '@lodev09/react-native-true-sheet/reanimated'
import Animated, { useAnimatedStyle, interpolate, Extrapolation } from 'react-native-reanimated'
function MyComponent() {
const { animatedPosition, animatedIndex, animatedDetent } = useReanimatedTrueSheet()
// Example: Move component based on sheet position
const positionStyle = useAnimatedStyle(() => ({
transform: [{ translateY: -animatedPosition.value }]
}))
// Example: Fade in as sheet expands from index 0 to 1
const opacityStyle = useAnimatedStyle(() => ({
opacity: interpolate(animatedIndex.value, [0, 1], [0, 1], Extrapolation.CLAMP)
}))
return (
This moves and fades with the sheet
)
}
```
#### Available Values [#available-values]
| Value | Type | Description |
| ------------------ | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `animatedPosition` | `SharedValue` | The current Y position of the sheet relative to the screen. |
| `animatedIndex` | `SharedValue` | The current detent index as a continuous float. Interpolates smoothly between detent indices during drag (e.g., `0.5` when halfway between index 0 and 1). |
| `animatedDetent` | `SharedValue` | The current detent value (0-1 fraction of screen height). Interpolates smoothly between detent values as the sheet is dragged. |
## Examples [#examples]
See the [example app](https://github.com/lodev09/react-native-true-sheet/tree/main/example) for complete implementations.
# Resizing Programmatically (/guides/resizing)
`TrueSheet` has a main prop called [`detents`](../reference/configuration#detents) which allows you to define the detents that the sheet can support. This is an array of [`SheetDetent`](../reference/types#sheetdetent) that supports values like `"auto"`, `"peek"`, or fractional numbers (0-1).
In some cases, you may want to resize the sheet programmatically for a better experience.
## How? [#how]
### Resize Programmatically [#resize-programmatically]
Define the sheet and use the [`resize`](../reference/methods#resize) method.
```tsx {2-2,6-6,11-13}
const App = () => {
const sheet = useRef(null)
const resize = async () => {
// Resize to 69%
await sheet.current?.resize(1)
console.log('Yay, we are now at 69% 💦')
}
return (
)
}
```
You can also do it globally using the related [global method](../reference/methods#global-methods).
```tsx
TrueSheet.resize('resizing-sheet', 1)
```
`detents` can only support up to 3 detents. ***collapsed***, ***half-expanded***, and ***expanded***.
Use [`insetAdjustment="never"`](../reference/configuration#insetadjustment) to disable automatic bottom inset adjustment on both platforms.
If you want to disable user dragging and only allow programmatic resizing, set [`draggable={false}`](../reference/configuration#draggable).
### Listening to Detent Change [#listening-to-detent-change]
If you want to get the active detent information, you can listen to detent changes by providing the [`onDetentChange`](../reference/events#ondetentchange) event.
The event comes with the [`DetentInfoEventPayload`](../reference/types#detentinfoeventpayload) that provides the detent `index` and `position` (Y position on screen).
Use the `index` to reference the detent from your `detents` array. For example, if `detents={['auto', 0.69, 1]}` and `index` is `1`, the active detent is `0.69`.
```tsx {9-11,17-17}
const App = () => {
const sheet = useRef(null)
const resize = async () => {
// Resize to 69%
await sheet.current?.resize(1)
}
const handleDetentChange = (e: DetentChangeEvent) => {
const { index, position } = e.nativeEvent
console.log('Detent index:', index, 'position:', position) ✅
}
return (
)
}
```
The event will also trigger when the user drags the sheet into a detent.
# Scrolling Content (/guides/scrolling)
Scrolling content within the sheet requires some special configuration on both iOS and Android. Let's admit, some things just don't work out of the box.
Follow the guide below so you can scroll your content using `TrueSheet`.
## How? [#how]
Enable the [`scrollable`](../reference/configuration#scrollable) prop on your `TrueSheet` to properly handle scrolling within the sheet.
```tsx {5-7}
const App = () => {
const sheet = useRef(null)
return (
{/* Header content */}
)
}
```
By default, `scrollable` is `false`. Set it to `true` to enable automatic ScrollView fitting.
The [`auto`](../reference/types#sheetdetent) detent is not supported with `scrollable`. Use fixed fractional detents (e.g., `0.5`, `0.8`, `1`) instead.
### iOS [#ios]
On iOS, `scrollable` automatically pins scroll views (including `ScrollView` and `FlatList`) to fit within the sheet's available space. The ScrollView's top edge will be pinned below any top sibling views, and its left, right, and bottom edges will be pinned to the container.
The scroll view detection supports up to 2 levels deep in the view hierarchy, so you can wrap your `ScrollView` or `FlatList` in a container `View` if needed:
```tsx
{/* Header content */}
```
### Android [#android]
On Android, `scrollable` ensures the scroll view fills the available sheet space. Nested scrolling is automatically enabled on the detected scroll view, so you don't need to set `nestedScrollEnabled` manually.
When `scrollable` is enabled, any `nestedScrollEnabled` prop on your `ScrollView` or `FlatList` is ignored — TrueSheet manages nested scrolling internally. `RefreshControl` is also fully supported.
## Preventing Scroll-to-Expand [#preventing-scroll-to-expand]
By default, scrolling content to the top edge expands the sheet to the next detent. To disable this (e.g., for YouTube-style comments sheets where only the grabber should expand the sheet), set `scrollingExpandsSheet` to `false`:
```tsx
```
On iOS, this uses [`prefersScrollingExpandsWhenScrolledToEdge`](https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller/3858107-prefersscrollingexpandswhenscrol). On Android, a custom `BottomSheetBehavior` selectively blocks scroll-driven expansion while preserving normal scrolling, fling deceleration, and grabber-based dragging.
## Scroll Edge Effect [#scroll-edge-effect]
On iOS 26+, you can apply a blur/gradient edge effect on the header and footer when content scrolls behind them. This uses [`UIScrollEdgeElementContainerInteraction`](https://developer.apple.com/documentation/uikit/uiscrolledgeelementcontainerinteraction) under the hood.
Set [`topScrollEdgeEffect`](../reference/types#scrollableoptions) and/or [`bottomScrollEdgeEffect`](../reference/types#scrollableoptions) in [`scrollableOptions`](../reference/configuration#scrollableoptions) to enable it:
```tsx
}
footer={}
>
```
Available values: `'automatic'`, `'hard'`, `'soft'`, or `'hidden'` (default). See [`ScrollEdgeEffect`](../reference/types#scrolledgeeffect).
This also controls the scroll view's own edge effects ([`UIScrollEdgeEffect`](https://developer.apple.com/documentation/uikit/uiscrolledgeeffect)). `topScrollEdgeEffect` applies to both the header interaction and the scroll view's top edge, and `bottomScrollEdgeEffect` applies to both the footer interaction and the scroll view's bottom edge.
This feature requires iOS 26 or later. On older versions, these options are ignored.
## Using `scrollToEnd` [#using-scrolltoend]
When using `scrollable`, the native scroll view frame is modified to fit within the sheet container. This can cause `scrollToEnd()` on `FlatList` or `ScrollView` to not work as expected because React Native's JS-side scroll metrics may not reflect the actual native dimensions.
**Workaround:** Use `scrollToOffset` with a large value instead:
```tsx
const scrollRef = useRef(null);
const scrollToEnd = () => {
// Large offset will be clamped to max scrollable position
scrollRef.current?.scrollToOffset({ offset: 99999, animated: true });
};
return (
);
```
# Side Sheets (/guides/side-sheets)
Ever wanted your sheet to slide in from the side? Now it can! The [`anchor`](../reference/configuration#anchor) prop lets you dock the sheet to the left or right edge — perfect for iPad and tablet layouts.
## How? [#how]
Set [`anchor`](../reference/configuration#anchor) to `"left"` or `"right"` to position the sheet along that edge.
```tsx {5,6}
export const App = () => {
return (
)
}
```
Combine with [`maxContentWidth`](../reference/configuration#maxcontentwidth) to control the sheet width when anchored.
### Adjusting the Edge Offset [#adjusting-the-edge-offset]
On Android and Web, you can control the margin from the screen edge using [`anchorOffset`](../reference/configuration#anchoroffset). The default is `16`.
```tsx {5}
export const App = () => {
return (
)
}
```
On iOS, the system controls edge margins via `UISheetPresentationController`'s `sourceView`. The sheet will always float slightly from the edge due to system-defined margins.
`anchor` and `maxContentWidth` are ignored on phones in portrait orientation — the sheet always spans the full width.
# Stacking Sheets (/guides/stacking)
TrueSheet automatically handles stacking when you present a sheet while another is already visible. The currently visible sheet will be hidden, and when the new sheet is dismissed, the previous sheet will be shown again.
## How? [#how]
Simply present a new sheet while another is visible. The sheets don't need to be in a parent-child relationship - they can be defined anywhere in your component tree.
```tsx
const presentSheet2 = async () => {
await sheet2.current?.present() // Sheet 2 will present, Sheet 1 will be hidden
}
return (
<>
>
)
```
When Sheet 2 is dismissed, Sheet 1 will automatically be shown again.
The previous sheet is only hidden if it's not fully expanded. When expanded, the new sheet simply presents on top.
## Dismissing Stacked Sheets [#dismissing-stacked-sheets]
When you have stacked sheets, calling `dismiss()` will dismiss the sheet and all sheets presented on top of it in a cascade animation.
```tsx
// If Sheet 1 → Sheet 2 → Sheet 3 (Sheet 1 presented Sheet 2, Sheet 2 presented Sheet 3)
await sheet1.current?.dismiss() // Dismisses Sheet 1, Sheet 2, and Sheet 3
await sheet2.current?.dismiss() // Dismisses Sheet 2 and Sheet 3, Sheet 1 stays
await sheet3.current?.dismiss() // Dismisses Sheet 3, Sheet 1 and Sheet 2 stay
```
### Dismiss Only Child Sheets [#dismiss-only-child-sheets]
If you want to dismiss only the sheets on top while keeping the current sheet presented, use the [`dismissStack()`](../reference/methods#dismissstack) method:
```tsx
// If Sheet 1 → Sheet 2 → Sheet 3
await sheet1.current?.dismissStack() // Dismisses Sheet 2 and Sheet 3, Sheet 1 stays
await sheet2.current?.dismissStack() // Dismisses Sheet 3, Sheet 1 and Sheet 2 stay
```
This is useful when you need to dismiss all child sheets and return focus to a parent sheet without closing it.
If no sheets are presented on top, `dismissStack()` does nothing.
### Dismiss All Sheets [#dismiss-all-sheets]
To dismiss all sheets in the current stack at once, use the static [`dismissAll()`](../reference/methods#dismissall) method:
```tsx
await TrueSheet.dismissAll()
```
Presentation Context
This only dismisses sheets in the current presentation context. Sheets presented behind a modal (e.g., React Navigation modal or React Native Modal) will not be affected.
## Focus Events [#focus-events]
When stacking sheets, you can listen to focus events to know when a sheet gains or loses focus:
* **`onDidBlur`** - Called when the sheet loses focus because another sheet is presented on top
* **`onDidFocus`** - Called when the sheet regains focus after a sheet on top is dismissed
This is useful for pausing/resuming updates, refreshing data, or managing interactions when the sheet is not the topmost.
```tsx {7-8}
const handleFocus = () => {
console.log('Sheet regained focus - refresh data')
}
const handleBlur = () => {
console.log('Sheet lost focus - pause updates')
}
return (
sheet2.current?.present()} title="Present Sheet 2" />
)
```
# Web Support (/guides/web)
Yes, `TrueSheet` supports web! It ships a vendored drawer implementation so the same API works across iOS, Android, and Web.
## Installation [#installation]
Install `@radix-ui/react-dialog` and `@radix-ui/react-presence` alongside `@lodev09/react-native-true-sheet`. They're declared as optional peer dependencies and provide the underlying dialog primitives for the web renderer.
npm
pnpm
yarn
bun
```bash
npm install @radix-ui/react-dialog @radix-ui/react-presence
```
```bash
pnpm add @radix-ui/react-dialog @radix-ui/react-presence
```
```bash
yarn add @radix-ui/react-dialog @radix-ui/react-presence
```
```bash
bun add @radix-ui/react-dialog @radix-ui/react-presence
```
## Setup [#setup]
Wrap your app with `TrueSheetProvider`:
```tsx {1,5-7}
import { TrueSheetProvider } from '@lodev09/react-native-true-sheet'
const App = () => {
return (
)
}
```
On native platforms, `TrueSheetProvider` is a simple pass-through component, so you can safely wrap your entire app without any performance impact.
## Controlling Sheets [#controlling-sheets]
On web, use the `useTrueSheet` hook or refs to control sheets. Static methods like `TrueSheet.present()` are not supported on web.
### Using the Hook [#using-the-hook]
Use the `useTrueSheet` hook to control sheets by name:
```tsx {1,4,7-8}
import { TrueSheet, useTrueSheet } from '@lodev09/react-native-true-sheet'
const MyComponent = () => {
const { present, dismiss, dismissAll } = useTrueSheet()
return (
<>
present('my-sheet')} title="Open" />
dismiss('my-sheet')} title="Close" />
Hello from the sheet!
>
)
}
```
### Using Refs [#using-refs]
You can also use refs to control sheets directly as you would on native.
## Detached Mode [#detached-mode]
Use the `detached` prop to render the sheet as a floating card, not attached to the bottom edge. Use `detachedOffset` to control the spacing from the bottom:
```tsx
{/* Content */}
```
# Configuration (/reference/configuration)
Configuration props available for `TrueSheet`. Extends `ViewProps`.
```tsx {2-5}
```
## `ref` [#ref]
We use `ref` to reference our sheet and call the [imperative methods](methods). Learn more about refs [here](https://react.dev/learn/referencing-values-with-refs).
## `detents` [#detents]
Array of detents you want the sheet to support. See [this guide](../guides/resizing) for example.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ------------------------------------ | ---------- | -- | -- | -- |
| [`SheetDetent[]`](types#sheetdetent) | `[0.5, 1]` | ✅ | ✅ | ✅ |
A sheet can only support up to **3** detents only! AKA ***collapsed***, ***half-expanded***, and ***expanded***.
It's recommended to sort detents from **smallest to largest**.
## `name` [#name]
The name to reference this sheet. It has to be ***unique***. You can then present this sheet globally using its name. See [this guide](../guides/global-methods) for example.
| Type | Default | 🍎 | 🤖 | 🌐 |
| -------- | ------- | -- | -- | -- |
| `string` | | ✅ | ✅ | ✅ |
## `backgroundColor` [#backgroundcolor]
The sheet's background color. Uses the system default when not provided.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ------------ | ---------------- | -- | -- | -- |
| `ColorValue` | *system default* | ✅ | ✅ | ✅ |
When not provided, iOS 26+ uses the default [Liquid Glass](../guides/liquid-glass) effect, while iOS 18 and below uses the system background color. Android uses Material Design 3's `colorSurfaceContainerLow` which automatically adapts to light/dark mode.
On iOS 26.1+, setting this prop will override the default [Liquid Glass](../guides/liquid-glass) effect.
## `backgroundBlur` [#backgroundblur]
The blur effect style on iOS. When set, applies a blur effect over the [`backgroundColor`](#backgroundcolor). Example: `"light"`, `"dark"`, etc.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ---------------------------------------- | ------- | -- | -- | -- |
| [`BackgroundBlur`](types#backgroundblur) | | ✅ | | |
On iOS 26.1+, setting this prop will override the default [Liquid Glass](../guides/liquid-glass) effect.
## `blurOptions` [#bluroptions]
Options for customizing the blur effect. Only applies when [`backgroundBlur`](#backgroundblur) is set.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ---------------------------------- | ------- | -- | -- | -- |
| [`BlurOptions`](types#bluroptions) | | ✅ | | |
```tsx
```
## `cornerRadius` [#cornerradius]
The sheet corner radius.
| Type | Default | 🍎 | 🤖 | 🌐 |
| -------- | ---------------- | -- | -- | -- |
| `number` | *system default* | ✅ | ✅ | ✅ |
When not provided, iOS uses the device's native corner radius automatically, while Android defaults to `16` (following Material Design 3 guidelines).
## `elevation` [#elevation]
The elevation (shadow depth) of the sheet.
| Type | Default | 🍎 | 🤖 | 🌐 |
| -------- | ------- | -- | -- | -- |
| `number` | `4` | | ✅ | ✅ |
## `maxContentHeight` [#maxcontentheight]
The absolute maximum height of the sheet content, regardless of detents.
| Type | Default | 🍎 | 🤖 | 🌐 |
| -------- | ------- | -- | -- | -- |
| `number` | | ✅ | ✅ | ✅ |
## `maxContentWidth` [#maxcontentwidth]
The maximum width of the sheet content.
| Type | Default | 🍎 | 🤖 | 🌐 |
| -------- | ------- | -- | -- | -- |
| `number` | | ✅ | ✅ | ✅ |
On Android and Web, the sheet defaults to a maximum width of `640dp`. Setting this prop overrides that default. On iOS, the sheet uses the system default width.
This prop is ignored on phones in portrait orientation — the sheet always spans the full width.
## `anchor` [#anchor]
Anchors the sheet to the specified horizontal edge.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ------------------------------- | ---------- | -- | -- | -- |
| `'left' \| 'center' \| 'right'` | `'center'` | ✅ | ✅ | ✅ |
On iOS, the sheet uses `sourceView` to control horizontal positioning. System-defined margins prevent fully flush edge attachment.
This prop is ignored on phones in portrait orientation.
## `anchorOffset` [#anchoroffset]
The offset from the screen edge. Only applies when [`anchor`](#anchor) is `'left'` or `'right'`.
| Type | Default | 🍎 | 🤖 | 🌐 |
| -------- | ------- | -- | -- | -- |
| `number` | `16` | | ✅ | ✅ |
## `dismissible` [#dismissible]
If set to `false`, the sheet will prevent interactive dismissal via dragging or clicking outside of it.
| Type | Default | 🍎 | 🤖 | 🌐 |
| --------- | ------- | -- | -- | -- |
| `boolean` | `true` | ✅ | ✅ | ✅ |
## `draggable` [#draggable]
If set to `false`, the sheet will disable dragging to resize. The sheet can only be resized programmatically using the [`resize`](methods#resize) method.
| Type | Default | 🍎 | 🤖 | 🌐 |
| --------- | ------- | -- | -- | -- |
| `boolean` | `true` | ✅ | ✅ | ✅ |
When `draggable` is `false`, the [`grabber`](#grabber) is automatically hidden.
## `dimmed` [#dimmed]
Specify whether the sheet background is dimmed. Set to `false` to allow interaction with the background components.
| Type | Default | 🍎 | 🤖 | 🌐 |
| --------- | ------- | -- | -- | -- |
| `boolean` | `true` | ✅ | ✅ | ✅ |
## `dimmedDetentIndex` [#dimmeddetentindex]
The detent index that the sheet should start to dim the background.
| Type | Default | 🍎 | 🤖 | 🌐 |
| -------- | ------- | -- | -- | -- |
| `number` | `0` | ✅ | ✅ | ✅ |
This is ignored if `dimmed` is set to `false`.
## `initialDetentIndex` [#initialdetentindex]
Initially present the sheet, after mounting, at a given detent index.
| Type | Default | 🍎 | 🤖 | 🌐 |
| -------- | ------- | -- | -- | -- |
| `number` | `-1` | ✅ | ✅ | ✅ |
## `initialDetentAnimated` [#initialdetentanimated]
Specify whether the sheet should animate after mounting.
Used with `initialDetentIndex`.
| Type | Default | 🍎 | 🤖 | 🌐 |
| --------- | ------- | -- | -- | -- |
| `boolean` | `true` | ✅ | ✅ | ✅ |
## `grabber` [#grabber]
Shows a native grabber (or drag handle) on the sheet.
| Type | Default | 🍎 | 🤖 | 🌐 |
| --------- | ------- | -- | -- | -- |
| `boolean` | `true` | ✅ | ✅ | ✅ |
iOS uses the native `UISheetPresentationController` grabber by default, while Android renders a native view following [Material Design 3 specifications](https://m3.material.io/components/bottom-sheets/specs) (32x4dp, centered, with the standard drag handle color).
When [`grabberOptions`](#grabberoptions) is provided, iOS uses a custom grabber view instead of the system default.
## `grabberOptions` [#grabberoptions]
Options for customizing the grabber appearance. Only applies when [`grabber`](#grabber) is `true`.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ---------------------------------------- | ------- | -- | -- | -- |
| [`GrabberOptions`](types#grabberoptions) | | ✅ | ✅ | |
```tsx
```
On iOS, when `grabberOptions` is not provided, the native system grabber is used. When any option is provided, a custom grabber view with vibrancy effect is rendered instead.
## `accessibilityOptions` [#accessibilityoptions]
Options for customizing (e.g. localizing) the accessibility strings announced by screen readers.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ---------------------------------------------------- | ------- | -- | -- | -- |
| [`AccessibilityOptions`](types#accessibilityoptions) | | ✅ | ✅ | ✅ |
```tsx
```
On iOS, the grabber strings only apply when [`grabberOptions`](#grabberoptions) is provided. Without it, iOS uses the system grabber which is already localized by the system.
## `header` [#header]
A component that is fixed at the top of the sheet content. Useful for search bars, titles, or other header content. The header height is automatically accounted for in layout calculations. Accepts a functional `Component` or `ReactElement`. See [this guide](../guides/header) for example.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ------------------------------------ | ------- | -- | -- | -- |
| `ComponentType<...> \| ReactElement` | | ✅ | ✅ | ✅ |
## `headerStyle` [#headerstyle]
Style for the header container.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ---------------------- | ------- | -- | -- | -- |
| `StyleProp` | | ✅ | ✅ | ✅ |
## `footer` [#footer]
A component that floats at the bottom of the sheet. Accepts a functional `Component` or `ReactElement`.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ------------------------------------ | ------- | -- | -- | -- |
| `ComponentType<...> \| ReactElement` | | ✅ | ✅ | ✅ |
## `footerStyle` [#footerstyle]
Style for the footer container.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ---------------------- | ------- | -- | -- | -- |
| `StyleProp` | | ✅ | ✅ | ✅ |
## `footerOptions` [#footeroptions]
Options for customizing footer behavior.
| Type | Default | 🍎 | 🤖 | 🌐 |
| -------------------------------------- | ------- | -- | -- | -- |
| [`FooterOptions`](types#footeroptions) | | ✅ | ✅ | |
## `scrollable` [#scrollable]
On iOS, automatically pins ScrollView or FlatList to fit within the sheet's available space. When enabled, the ScrollView's top edge will be pinned below any top sibling views, and its left, right, and bottom edges will be pinned to the container. See [this guide](../guides/scrolling) for example.
On Android, it ensures the scroll view fills the available sheet space and automatically manages nested scrolling. Any `nestedScrollEnabled` prop on your `ScrollView` or `FlatList` is ignored when `scrollable` is enabled.
| Type | Default | 🍎 | 🤖 | 🌐 |
| --------- | ------- | -- | -- | -- |
| `boolean` | `false` | ✅ | ✅ | |
The [`auto`](types#sheetdetent) detent is not supported with `scrollable`. Use fixed fractional detents (e.g., `0.5`, `0.8`, `1`) instead.
## `scrollableOptions` [#scrollableoptions]
Options for customizing scrollable behavior. Only applies when `scrollable` is `true`.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ---------------------------------------------- | ------- | -- | -- | -- |
| [`ScrollableOptions`](types#scrollableoptions) | | ✅ | ✅ | |
## `presentation` [#presentation]
Controls the sheet presentation style on iPad and web (landscape/tablet).
* `'page'`: bottom-attached page sheet (full or readable width).
* `'form'`: centered floating form sheet (default form-sheet width).
`'form'` is absolute — [`maxContentWidth`](#maxcontentwidth) is ignored when set.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ------------------ | -------- | --------- | -- | -- |
| `'page' \| 'form'` | `'page'` | ***17+*** | | ✅ |
## `insetAdjustment` [#insetadjustment]
Controls how the bottom safe area inset affects detent heights.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ------------------------------------------ | ------------- | -- | -- | -- |
| [`InsetAdjustment`](types#insetadjustment) | `"automatic"` | ✅ | ✅ | |
## `detached` [#detached]
Renders the sheet as a detached floating card, not attached to the bottom edge.
| Type | Default | 🍎 | 🤖 | 🌐 |
| --------- | ------- | -- | -- | -- |
| `boolean` | `false` | | | ✅ |
## `detachedOffset` [#detachedoffset]
The offset from the bottom edge when [`detached`](#detached) is enabled.
| Type | Default | 🍎 | 🤖 | 🌐 |
| -------- | ------- | -- | -- | -- |
| `number` | `16` | | | ✅ |
## `style` [#style]
The sheet's container style override.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ---------------------- | ------- | -- | -- | -- |
| `StyleProp` | | ✅ | ✅ | ✅ |
# Lifecycle Events (/reference/events)
Lifecycle events available for `TrueSheet`.
```tsx
const App = () => {
const handleDismiss = () => {
console.log('Bye bye')
}
const handleOnWillPresent = (e: WillPresentEvent) => {
console.log(e.nativeEvent) // { index: 0, position: 123.5, detent: 0.5 }
console.log('Sheet is about to be presented')
}
const handleOnDidPresent = (e: DidPresentEvent) => {
console.log(e.nativeEvent) // { index: 0, position: 123.5, detent: 0.5 }
}
const handleDetentChange = (e: DetentChangeEvent) => {
console.log(e.nativeEvent) // { index: 1, position: 234.5, detent: 0.8 }
}
return (
sheet.current?.present()} />
sheet.current?.resize(1)} />
sheet.current?.dismiss(1)} />
)
}
```
## `onMount` [#onmount]
Called when the sheet's content is mounted and ready.
The sheet automatically waits for this event before presenting.
## `onWillPresent` [#onwillpresent]
Comes with [`DetentInfoEventPayload`](types#detentinfoeventpayload).
This is called when the sheet is about to be presented.
## `onDidPresent` [#ondidpresent]
Comes with [`DetentInfoEventPayload`](types#detentinfoeventpayload).
This is called when the sheet has been presented, providing the detent `index` and `position`.
## `onWillDismiss` [#onwilldismiss]
This is called when the sheet is about to be dismissed.
## `onDidDismiss` [#ondiddismiss]
This is called when the sheet has been dismissed.
## `onDetentChange` [#ondetentchange]
Comes with [`DetentInfoEventPayload`](types#detentinfoeventpayload).
This is called when the detent of the sheet has changed, either by dragging or by programmatically [resizing](methods#resize) it.
## `onDragBegin` [#ondragbegin]
Comes with [`DetentInfoEventPayload`](types#detentinfoeventpayload).
This is called when the sheet has began dragging.
## `onDragChange` [#ondragchange]
Comes with [`DetentInfoEventPayload`](types#detentinfoeventpayload).
This is called when the sheet is being dragged.
## `onDragEnd` [#ondragend]
Comes with [`DetentInfoEventPayload`](types#detentinfoeventpayload).
This is called when the sheet dragging has ended.
## `onPositionChange` [#onpositionchange]
Comes with [`PositionChangeEventPayload`](types#positionchangeeventpayload).
This is called continuously when the sheet's position changes during drag operations or transitions. This event fires more frequently than `onDragChange` and provides real-time position updates.
The `realtime` property indicates whether the position value is real-time (e.g., during drag or animation tracking). When `realtime` is `false`, the position should be animated in JS (ReanimatedTrueSheet handles this automatically).
Use this event when you need smooth, continuous position tracking for animations or visual feedback. For less frequent updates during dragging, use `onDragChange` instead.
## `onWillFocus` [#onwillfocus]
This is called when the sheet is about to gain focus. This happens:
* When the sheet is being presented (along with `onWillPresent`)
* When another sheet presented on top of it begins dismissing
## `onDidFocus` [#ondidfocus]
This is called when the sheet has gained focus. This happens:
* When the sheet has been presented (along with `onDidPresent`)
* When another sheet presented on top of it is dismissed
Useful for refreshing data or re-enabling interactions.
## `onWillBlur` [#onwillblur]
This is called when the sheet is about to lose focus. This happens:
* When the sheet is being dismissed (along with `onWillDismiss`)
* When another sheet is about to be presented on top of it
## `onDidBlur` [#ondidblur]
This is called when the sheet has lost focus. This happens:
* When the sheet has been dismissed (along with `onDidDismiss`)
* When another sheet is presented on top of it
Useful for pausing updates or disabling interactions.
## `onBackPress` [#onbackpress]
Android only
This is called when the hardware back button is pressed on Android. Use this to handle custom back press behavior, such as dismissing the sheet programmatically when `dismissible` is `false`.
# Imperative Methods (/reference/methods)
## Component Methods [#component-methods]
`TrueSheet` has a couple of core methods that you'll be using most of the time. These methods are asynchronous so you can use `async` and `await` on them.
Define the sheet and provide a `ref`. We will be using the `sheet` to access these methods.
```tsx {4}
const sheet = useRef(null)
return (
)
```
### `present` [#present]
Present the sheet. Optionally accepts a detent `index` and `animated` flag. See [`detents`](configuration#detents) prop.
| Parameters | Required | Default |
| ------------------- | -------- | ------- |
| `index: number` | No | `0` |
| `animated: boolean` | No | `true` |
```tsx
await sheet.current?.present()
// Present without animation
await sheet.current?.present(0, false)
```
### `dismiss` [#dismiss]
Dismisses the sheet and all sheets presented on top of it in a single animation. Optionally accepts an `animated` flag.
| Parameters | Required | Default |
| ------------------- | -------- | ------- |
| `animated: boolean` | No | `true` |
```tsx
await sheet.current?.dismiss()
// Dismiss without animation
await sheet.current?.dismiss(false)
```
### `dismissStack` [#dismissstack]
Dismisses only the sheets presented on top of this sheet, keeping this sheet presented. If no sheets are presented on top, this method does nothing.
| Parameters | Required | Default |
| ------------------- | -------- | ------- |
| `animated: boolean` | No | `true` |
```tsx
await sheet.current?.dismissStack()
// Dismiss stack without animation
await sheet.current?.dismissStack(false)
```
This is useful when you want to close child sheets while keeping the current sheet open.
### `resize` [#resize]
Resizes the sheet programmatically by `index`. The sheet must be presented before calling this method.
| Parameters | Required |
| --------------- | -------- |
| `index: number` | Yes |
```tsx
// Resize to 80%
await sheet.current?.resize(1)
```
## Global Methods [#global-methods]
You can also call the above methods statically without having access to a sheet's `ref`. This is particularly useful when you want to present a sheet from anywhere.
The API is similar to the component methods except for the required [`name`](configuration#name) prop. See [this guide](../guides/global-methods) for example.
In the example below, we provide `"my-sheet"` as the `name` of our sheet.
```tsx {4}
const sheet = useRef(null)
return (
)
```
You can then use these methods globally.
```tsx
await TrueSheet.present('my-sheet')
// Present without animation
await TrueSheet.present('my-sheet', 0, false)
```
```tsx
await TrueSheet.dismiss('my-sheet')
// Dismiss without animation
await TrueSheet.dismiss('my-sheet', false)
```
```tsx
await TrueSheet.dismissStack('my-sheet')
// Dismiss stack without animation
await TrueSheet.dismissStack('my-sheet', false)
```
If no sheets are presented on top, `dismissStack()` does nothing.
```tsx
// Resize to 80%
await TrueSheet.resize('my-sheet', 0.8)
```
### `dismissAll` [#dismissall]
Dismisses all presented sheets in the current stack. This is useful when you need to close all sheets at once, for example when navigating away or resetting the UI state.
| Parameters | Required | Default |
| ------------------- | -------- | ------- |
| `animated: boolean` | No | `true` |
```tsx
await TrueSheet.dismissAll()
// Dismiss all without animation
await TrueSheet.dismissAll(false)
```
This only dismisses sheets in the current presentation context. Sheets presented behind a modal (e.g., React Navigation modal or React Native Modal) will not be affected.
### Web [#web]
Static methods are not supported on web. Use the `useTrueSheet()` hook instead.
```tsx
import { useTrueSheet } from '@lodev09/react-native-true-sheet'
const { present, dismiss, dismissStack, resize, dismissAll } = useTrueSheet()
await present('my-sheet')
await dismiss('my-sheet')
await dismissAll()
```
# Types (/reference/types)
## `SheetDetent` [#sheetdetent]
```tsx
```
| Value | Description | 🍎 | 🤖 | 🌐 |
| -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -- | -- |
| `"auto"` | Auto resize based on content height. | ***16+*** | ✅ | ✅ |
| `"peek"` | Collapsed height based on the combined [`header`](configuration#header) and [`footer`](configuration#footer) heights, plus the content up to the bottom of a [`TrueSheetPeek`](../guides/peeking) component rendered within it. Falls back to a fixed height of `150` when none is provided. | ***16+*** | ✅ | ✅ |
| `number` | Fractional height (0-1) representing percentage of screen height. | ✅ | ✅ | ✅ |
The `"auto"` detent is not supported with [`scrollable`](configuration#scrollable). Use fixed fractional detents (e.g., `0.5`, `0.8`, `1`) instead.
## `BackgroundBlur` [#backgroundblur]
Blur style that is mapped into native values in iOS.
```tsx
```
| Value |
| ------------------------------------ |
| `"light"` |
| `"dark"` |
| `"default"` |
| `"extra-light"` |
| `"regular"` |
| `"prominent"` |
| `"system-ultra-thin-material"` |
| `"system-thin-material"` |
| `"system-material"` |
| `"system-thick-material"` |
| `"system-chrome-material"` |
| `"system-ultra-thin-material-light"` |
| `"system-thin-material-light"` |
| `"system-material-light"` |
| `"system-thick-material-light"` |
| `"system-chrome-material-light"` |
| `"system-ultra-thin-material-dark"` |
| `"system-thin-material-dark"` |
| `"system-material-dark"` |
| `"system-thick-material-dark"` |
| `"system-chrome-material-dark"` |
## `BlurOptions` [#bluroptions]
Options for customizing the blur effect. Only applies when `backgroundBlur` is set.
```tsx
```
| Property | Type | Description | Default |
| ------------- | --------- | ----------------------------------------------------------------------------------------------------------- | ---------------- |
| `intensity` | `number` | The intensity of the blur effect (0-100). | *system default* |
| `interaction` | `boolean` | Enables or disables user interaction on the blur view. Disabling can help with visual artifacts on iOS 18+. | `true` |
## `ScrollableOptions` [#scrollableoptions]
Options for customizing scrollable behavior. Only applies when `scrollable` is `true`.
```tsx
```
| Property | Type | Description | Default |
| ------------------------ | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- |
| `scrollingExpandsSheet` | `boolean` | When `false`, scrolling the content does not expand the sheet to the next detent. Only dragging the grabber or sheet background expands it. Useful for YouTube-style comments sheets. | `true` |
| `keyboardScrollOffset` | `number` | Additional offset when scrolling to a focused input above the keyboard. | `0` |
| `topScrollEdgeEffect` | [`ScrollEdgeEffect`](#scrolledgeeffect) | The scroll edge effect applied to the top edge of the scroll view and header. iOS 26+ only. | `'hidden'` |
| `bottomScrollEdgeEffect` | [`ScrollEdgeEffect`](#scrolledgeeffect) | The scroll edge effect applied to the bottom edge of the scroll view and footer. iOS 26+ only. | `'hidden'` |
## `FooterOptions` [#footeroptions]
Options for customizing footer behavior.
```tsx
}
footerOptions={{
keyboardOffset: -insets.bottom,
}}
>
{/* ... */}
```
| Property | Type | Description | Default |
| ---------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `keyboardOffset` | `number` | Adjusts how far the footer rises when the keyboard opens. Positive values raise it higher; pass `-insets.bottom` to tuck its safe-area padding behind the keyboard instead of leaving a gap. | `0` |
## `ScrollEdgeEffect` [#scrolledgeeffect]
Controls the blur/gradient edge effect on the scroll view edges and header/footer overlay views. iOS 26+ only.
| Value | Description |
| ------------- | ------------------------------------ |
| `'automatic'` | System default edge effect style. |
| `'hard'` | A hard, opaque edge effect. |
| `'soft'` | A soft, gradient edge effect. |
| `'hidden'` | No edge effect. This is the default. |
## `GrabberOptions` [#grabberoptions]
Options for customizing the grabber (drag handle) appearance.
```tsx
```
| Property | Type | Description | Default |
| -------------- | ------------ | ------------------------------------------------------------------------------------------------------ | ------------------------ |
| `width` | `number` | The width of the grabber pill. | iOS: `36`, Android: `32` |
| `height` | `number` | The height of the grabber pill. | iOS: `5`, Android: `4` |
| `topMargin` | `number` | The top margin from the sheet edge. | iOS: `5`, Android: `16` |
| `cornerRadius` | `number` | The corner radius of the grabber pill. | `height / 2` |
| `color` | `ColorValue` | The color of the grabber. Uses native styling when not provided. | |
| `adaptive` | `boolean` | Whether the grabber adapts to light/dark mode. Uses vibrancy on iOS and theme-based colors on Android. | `true` |
## `AccessibilityOptions` [#accessibilityoptions]
Options for customizing (e.g. localizing) the accessibility strings announced by screen readers.
```tsx
```
| Property | Type | Description | Default |
| --------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| `grabberLabel` | `string` | The accessibility label of the grabber. | iOS: `'Sheet Grabber'`, Android: `'Drag handle'` |
| `grabberHint` | `string` | The accessibility hint of the grabber. iOS only. | `'Double-tap to expand. Swipe up or down to resize the sheet'` |
| `expandedValue` | `string` | The value announced when the sheet is at the last detent. | `'Expanded'` |
| `collapsedValue` | `string` | The value announced when the sheet is at the first detent. | `'Collapsed'` |
| `detentValue` | `string` | The value announced at intermediate detents. Supports `{index}` and `{count}` placeholders. | `'Detent {index} of {count}'` |
| `expandActionLabel` | `string` | The label of the expand accessibility action. Android only. | `'Expand'` |
| `collapseActionLabel` | `string` | The label of the collapse accessibility action. Android only. | `'Collapse'` |
| `paneTitle` | `string` | The title announced when the sheet appears. Sets the accessibility pane title on Android and the hidden dialog title on Web. | Android: `'Bottom sheet'`, Web: `'Sheet'` |
On iOS, the grabber strings only apply when [`grabberOptions`](configuration#grabberoptions) is provided. Without it, iOS uses the system grabber which is already localized by the system.
## `InsetAdjustment` [#insetadjustment]
Controls how the bottom safe area inset affects detent heights.
```tsx
```
| Value | Description |
| ------------- | ------------------------------------------------------------------ |
| `"automatic"` | System handles insets automatically. This is the default behavior. |
| `"never"` | TrueSheet will keep the layout as-is for precise sizing. |
## `DetentInfoEventPayload` [#detentinfoeventpayload]
`Object` that comes with most sheet events.
```tsx
{
index: 1,
position: 123.5,
detent: 0.5
}
```
| Property | Type | Description |
| -------- | -------- | --------------------------------------------------------------- |
| index | `number` | The detent index from the provided detents. See `detents` prop. |
| position | `number` | The Y position of the sheet relative to the screen. |
| detent | `number` | The detent value (0-1) for the current index. |
## `PositionChangeEventPayload` [#positionchangeeventpayload]
`Object` that comes with the `onPositionChange` event. Extends [`DetentInfoEventPayload`](#detentinfoeventpayload).
```tsx
{
index: 1.5,
position: 123.5,
detent: 0.5,
realtime: true
}
```
| Property | Type | Description |
| -------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| index | `number` | The interpolated detent index. Continuous value that smoothly transitions between detents (e.g., `0.5` means halfway between detent 0 and 1). |
| position | `number` | The Y position of the sheet relative to the screen. |
| detent | `number` | The detent value (0-1) for the nearest detent index. |
| realtime | `boolean` | Whether the position is a real-time value (e.g., during drag or animation tracking). When `false`, position should be animated in JS. |
# Configuration (/next/reference/configuration)
Configuration props available for `TrueSheet`. Extends `ViewProps`.
```tsx {2-5}
```
## `ref` [#ref]
We use `ref` to reference our sheet and call the [imperative methods](methods). Learn more about refs [here](https://react.dev/learn/referencing-values-with-refs).
## `detents` [#detents]
Array of detents you want the sheet to support. See [this guide](../guides/resizing) for example.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ------------------------------------ | ---------- | -- | -- | -- |
| [`SheetDetent[]`](types#sheetdetent) | `[0.5, 1]` | ✅ | ✅ | ✅ |
A sheet can only support up to **3** detents only! AKA ***collapsed***, ***half-expanded***, and ***expanded***.
It's recommended to sort detents from **smallest to largest**.
## `name` [#name]
The name to reference this sheet. It has to be ***unique***. You can then present this sheet globally using its name. See [this guide](../guides/global-methods) for example.
| Type | Default | 🍎 | 🤖 | 🌐 |
| -------- | ------- | -- | -- | -- |
| `string` | | ✅ | ✅ | ✅ |
## `backgroundColor` [#backgroundcolor]
The sheet's background color. Uses the system default when not provided.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ------------ | ---------------- | -- | -- | -- |
| `ColorValue` | *system default* | ✅ | ✅ | ✅ |
Without a color or blur, iOS 26+ uses [Liquid Glass](../guides/liquid-glass), while earlier iOS versions use system-material blur. Android and Web use Material Design 3's `colorSurfaceContainerLow`, which adapts to light/dark mode.
On supported iOS 26.1+ devices, `backgroundColor` uses a native color effect. It does not tint Liquid Glass. Other devices paint the background color.
On iOS, [`backgroundBlur`](#backgroundblur) takes precedence over `backgroundColor`. Use [`backgroundStyle.backgroundColor`](#backgroundstyle) to tint glass or blur, or for an exact opaque color over full-screen modals.
## `detentBackgrounds` [#detentbackgrounds]
Backgrounds matched to [`detents`](#detents) by index. Each entry is a color string or an object with either `color` or `blur`.
A string such as `"#18202b"` is shorthand for `{ color: "#18202b" }`.
`null`, `undefined`, and missing entries inherit `backgroundColor`/`backgroundBlur`.
An entry replaces the sheet background, so a color entry overrides the sheet blur.
| Type | Default | 🍎 | 🤖 | 🌐 |
| -------------------------------------------------------- | ------- | -- | -- | -- |
| [`(DetentBackground \| null)[]`](types#detentbackground) | | ✅ | ✅ | ✅ |
```tsx
```
| Platform | Transition | Blur entries |
| ------------------------------------ | ----------------------------------------------------------- | ----------------------------- |
| iOS 26.1+ with native effect support | UIKit transitions as the sheet moves | Native blur effect |
| Other iOS devices | Cross-fade after passing the midpoint between detents | Blur view at system intensity |
| Android and Web | Color cross-fade after passing the midpoint between detents | Inherit the sheet background |
The midpoint rule applies in both directions, including `resize()`.
Presentation starts with the entry at the presented index.
`background` and `backgroundStyle` remain above these effects and apply across all detents.
An opaque background wrapper hides the detent backgrounds.
See [per-detent backgrounds](../guides/backgrounds#per-detent-backgrounds) for Liquid Glass limitations.
## `background` [#background]
A custom background behind the header, content, and footer. Accepts a `ReactNode`.
The wrapper fills the sheet, sits above the sheet background effects, and ignores touches.
It does not affect content height or detent calculations.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ----------- | ------- | -- | -- | -- |
| `ReactNode` | | ✅ | ✅ | ✅ |
Size the element to fill the wrapper:
```tsx
}>
Sheet content
```
For a custom blur, see the [Backgrounds guide](../guides/backgrounds#custom-blur).
## `backgroundStyle` [#backgroundstyle]
Style for the [`background`](#background) wrapper. Only `backgroundColor` is supported.
The style works without a `background` element and applies across all detents.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ----------------------------------------------- | ------- | -- | -- | -- |
| `StyleProp>` | | ✅ | ✅ | ✅ |
Use an opaque color for an exact background color, including over full-screen modals.
Use a translucent color to tint the glass or blur:
```tsx
Sheet content
```
## `backgroundBlur` [#backgroundblur]
The blur effect style on iOS, such as `"light"`, `"dark"`, or `"system-material"`. It takes precedence over [`backgroundColor`](#backgroundcolor).
Use [`backgroundStyle.backgroundColor`](#backgroundstyle) to tint the blur.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ---------------------------------------- | ------- | -- | -- | -- |
| [`BackgroundBlur`](types#backgroundblur) | | ✅ | | |
On supported iOS 26.1+ devices, this uses a native effect that replaces Liquid Glass.
Earlier iOS versions and devices without the setters use a non-interactive blur view at system intensity.
Design compatibility mode on iOS 26 also uses this fallback.
On iOS 27+, `UIDesignRequiresCompatibility` is ignored.
On iOS 26.0, this blur sits above Liquid Glass.
For custom intensity or blur on other platforms, use a third-party blur in [`background`](#background).
## `cornerRadius` [#cornerradius]
The sheet corner radius.
| Type | Default | 🍎 | 🤖 | 🌐 |
| -------- | ---------------- | -- | -- | -- |
| `number` | *system default* | ✅ | ✅ | ✅ |
When not provided, iOS uses the device's native corner radius automatically, while Android defaults to `16` (following Material Design 3 guidelines).
## `elevation` [#elevation]
The elevation (shadow depth) of the sheet.
| Type | Default | 🍎 | 🤖 | 🌐 |
| -------- | ------- | -- | -- | -- |
| `number` | `4` | | ✅ | ✅ |
## `maxContentHeight` [#maxcontentheight]
The absolute maximum height of the sheet content, regardless of detents.
| Type | Default | 🍎 | 🤖 | 🌐 |
| -------- | ------- | -- | -- | -- |
| `number` | | ✅ | ✅ | ✅ |
## `maxContentWidth` [#maxcontentwidth]
The maximum width of the sheet content.
| Type | Default | 🍎 | 🤖 | 🌐 |
| -------- | ------- | -- | -- | -- |
| `number` | | ✅ | ✅ | ✅ |
On Android and Web, the sheet defaults to a maximum width of `640dp`. Setting this prop overrides that default. On iOS, the sheet uses the system default width.
This prop is ignored on phones in portrait orientation — the sheet always spans the full width.
## `placement` [#placement]
Horizontal placement of the sheet. `'leading'` and `'trailing'` follow the layout direction (left/right in LTR, right/left in RTL).
| Type | Default | 🍎 | 🤖 | 🌐 |
| ---------------------------------------------------- | ------------- | -- | -- | -- |
| `'automatic' \| 'leading' \| 'center' \| 'trailing'` | `'automatic'` | ✅ | ✅ | ✅ |
`'automatic'` lets the system decide. It only differs from `'center'` on iOS 27+, where both map to `UISheetPresentationController`'s native `preferredPlacement`. On Android, Web, and iOS 26 and below, `'automatic'` and `'center'` both center the sheet.
On iOS 26 and below, edge placement uses `sourceView`. System-defined margins prevent fully flush edge attachment.
This prop is ignored on phones in portrait orientation.
## `placementOffset` [#placementoffset]
The offset from the screen edge. Only applies when [`placement`](#placement) is `'leading'` or `'trailing'`.
| Type | Default | 🍎 | 🤖 | 🌐 |
| -------- | ------- | -- | -- | -- |
| `number` | `16` | | ✅ | ✅ |
## `dismissible` [#dismissible]
If set to `false`, the sheet will prevent interactive dismissal via dragging or clicking outside of it.
| Type | Default | 🍎 | 🤖 | 🌐 |
| --------- | ------- | -- | -- | -- |
| `boolean` | `true` | ✅ | ✅ | ✅ |
Blocked attempts fire [`onDismissAttempt`](events#ondismissattempt). Use it to confirm before dismissing programmatically, e.g. when a form has unsaved changes.
## `dismissThreshold` [#dismissthreshold]
How far the sheet must be swiped down from its lowest detent to dismiss. The default `'half'` matches iOS. Use `'short'` for Compose Material3's `ModalBottomSheet` feel.
| Type | Default | 🍎 | 🤖 | 🌐 |
| -------------------------------------------- | -------- | -- | -- | -- |
| [`DismissThreshold`](types#dismissthreshold) | `'half'` | | ✅ | ✅ |
## `draggable` [#draggable]
If set to `false`, the sheet will disable dragging to resize. The sheet can only be resized programmatically using the [`resize`](methods#resize) method.
| Type | Default | 🍎 | 🤖 | 🌐 |
| --------- | ------- | -- | -- | -- |
| `boolean` | `true` | ✅ | ✅ | ✅ |
When `draggable` is `false`, the [`grabber`](#grabber) is automatically hidden.
On iOS 26+, `draggable={false}` doesn't turn off Liquid Glass's touch response: the sheet still stretches slightly and follows the finger while a touch moves over it (e.g. a slider inside the sheet). Set [`backgroundColor`](#backgroundcolor) to replace the glass with a native color effect on iOS 26.1+, which doesn't react to touches.
## `dimmed` [#dimmed]
Specify whether the sheet background is dimmed. Set to `false` to allow interaction with the background components.
| Type | Default | 🍎 | 🤖 | 🌐 |
| --------- | ------- | -- | -- | -- |
| `boolean` | `true` | ✅ | ✅ | ✅ |
## `dimColor` [#dimcolor]
The color of the dim behind the sheet.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ------------ | --------- | -- | -- | -- |
| `ColorValue` | `"black"` | ✅ | ✅ | ✅ |
This is ignored if `dimmed` is set to `false`.
## `dimOpacity` [#dimopacity]
The opacity of the dim behind the sheet, from `0` to `1`.
| Type | Default | 🍎 | 🤖 | 🌐 |
| -------- | ------- | -- | -- | -- |
| `number` | `0.5` | ✅ | ✅ | ✅ |
This is ignored if `dimmed` is set to `false`.
At `0`, the dim is invisible but still blocks touches to the background. Set [`dimmed`](#dimmed) to `false` to allow interaction instead.
On iOS, setting `dimColor` or `dimOpacity` replaces the system dim with a custom dim layer. Stacked sheets each add their own layer, so the background gets darker with every sheet.
## `dimmedDetentIndex` [#dimmeddetentindex]
The detent index that the sheet should start to dim the background.
| Type | Default | 🍎 | 🤖 | 🌐 |
| -------- | ------- | -- | -- | -- |
| `number` | `0` | ✅ | ✅ | ✅ |
This is ignored if `dimmed` is set to `false`.
## `initialDetentIndex` [#initialdetentindex]
Initially present the sheet, after mounting, at a given detent index.
| Type | Default | 🍎 | 🤖 | 🌐 |
| -------- | ------- | -- | -- | -- |
| `number` | `-1` | ✅ | ✅ | ✅ |
## `initialDetentAnimated` [#initialdetentanimated]
Specify whether the sheet should animate after mounting.
Used with `initialDetentIndex`.
| Type | Default | 🍎 | 🤖 | 🌐 |
| --------- | ------- | -- | -- | -- |
| `boolean` | `true` | ✅ | ✅ | ✅ |
## `lazy` [#lazy]
Specify whether the sheet content should mount lazily, on first presentation. Set to `false` to mount the content before presentation without presenting the sheet. Use this when content is not ready on the first render, then call [`present()`](methods#present) after your readiness signal so [`auto`](types#sheetdetent) detents measure the settled content.
| Type | Default | 🍎 | 🤖 | 🌐 |
| --------- | ------- | -- | -- | -- |
| `boolean` | `true` | ✅ | ✅ | |
Content that requires window attachment to measure, such as SwiftUI-hosted views, still only measures during presentation.
## `grabber` [#grabber]
Shows a native grabber (or drag handle) on the sheet.
| Type | Default | 🍎 | 🤖 | 🌐 |
| --------- | ------- | -- | -- | -- |
| `boolean` | `true` | ✅ | ✅ | ✅ |
iOS uses the native `UISheetPresentationController` grabber by default, while Android renders a native view following [Material Design 3 specifications](https://m3.material.io/components/bottom-sheets/specs) (32x4dp, centered, with the standard drag handle color).
When [`grabberOptions`](#grabberoptions) is provided, iOS uses a custom grabber view instead of the system default.
## `grabberOptions` [#grabberoptions]
Options for customizing the grabber appearance. Only applies when [`grabber`](#grabber) is `true`.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ---------------------------------------- | ------- | -- | -- | -- |
| [`GrabberOptions`](types#grabberoptions) | | ✅ | ✅ | |
```tsx
```
On iOS, when `grabberOptions` is not provided, the native system grabber is used. When any option is provided, a custom grabber view with vibrancy effect is rendered instead.
## `accessibilityOptions` [#accessibilityoptions]
Options for customizing (e.g. localizing) the accessibility strings announced by screen readers.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ---------------------------------------------------- | ------- | -- | -- | -- |
| [`AccessibilityOptions`](types#accessibilityoptions) | | ✅ | ✅ | ✅ |
```tsx
```
On iOS, the grabber strings only apply when [`grabberOptions`](#grabberoptions) is provided. Without it, iOS uses the system grabber which is already localized by the system.
## `header` [#header]
A component that is fixed at the top of the sheet content. Useful for search bars, titles, or other header content. The header height is automatically accounted for in layout calculations. Accepts a functional `Component` or `ReactElement`. See [this guide](../guides/header) for example.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ------------------------------------ | ------- | -- | -- | -- |
| `ComponentType<...> \| ReactElement` | | ✅ | ✅ | ✅ |
## `headerStyle` [#headerstyle]
Style for the header container.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ---------------------- | ------- | -- | -- | -- |
| `StyleProp` | | ✅ | ✅ | ✅ |
## `headerOptions` [#headeroptions]
Options for customizing header behavior.
| Type | Default | 🍎 | 🤖 | 🌐 |
| -------------------------------------- | ------- | -- | -- | -- |
| [`HeaderOptions`](types#headeroptions) | | ✅ | ✅ | ✅ |
## `footer` [#footer]
A component that is pinned at the bottom of the sheet. The footer height is automatically accounted for in layout calculations. Accepts a functional `Component` or `ReactElement`. See [this guide](../guides/footer) for example.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ------------------------------------ | ------- | -- | -- | -- |
| `ComponentType<...> \| ReactElement` | | ✅ | ✅ | ✅ |
## `footerStyle` [#footerstyle]
Style for the footer container.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ---------------------- | ------- | -- | -- | -- |
| `StyleProp` | | ✅ | ✅ | ✅ |
## `footerOptions` [#footeroptions]
Options for customizing footer behavior.
| Type | Default | 🍎 | 🤖 | 🌐 |
| -------------------------------------- | ------- | -- | -- | -- |
| [`FooterOptions`](types#footeroptions) | | ✅ | ✅ | ✅ |
## `scrollableRef` [#scrollableref]
A ref to the scrollable component (e.g. `ScrollView`, `FlatList`) rendered within the sheet content. Required for scrollable handling — nested scrolling, keyboard insets, and `auto` detent sizing are wired to this scrollable. See [this guide](../guides/scrolling) for more information.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ---------------------- | ------- | -- | -- | -- |
| `RefObject` | | ✅ | ✅ | ✅ |
## `scrollableOptions` [#scrollableoptions]
Options for customizing scrollable behavior. Applies to the scrollable provided via [`scrollableRef`](#scrollableref). See [this guide](../guides/scrolling) for more information.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ---------------------------------------------- | ------- | -- | -- | -- |
| [`ScrollableOptions`](types#scrollableoptions) | | ✅ | ✅ | ✅ |
## `presentation` [#presentation]
Controls the sheet presentation style on iPad and web (landscape/tablet).
* `'page'`: bottom-attached page sheet (full or readable width).
* `'form'`: centered floating form sheet (default form-sheet width).
`'form'` is absolute — [`maxContentWidth`](#maxcontentwidth) is ignored when set.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ------------------ | -------- | --------- | -- | -- |
| `'page' \| 'form'` | `'page'` | ***17+*** | | ✅ |
## `insetAdjustment` [#insetadjustment]
Controls how the bottom safe area inset affects detent heights.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ------------------------------------------ | ------------- | -- | -- | -- |
| [`InsetAdjustment`](types#insetadjustment) | `"automatic"` | ✅ | ✅ | |
## `detached` [#detached]
Renders the sheet as a detached floating card, not attached to the bottom edge.
| Type | Default | 🍎 | 🤖 | 🌐 |
| --------- | ------- | -- | -- | -- |
| `boolean` | `false` | | | ✅ |
## `detachedOffset` [#detachedoffset]
The offset from the bottom edge when [`detached`](#detached) is enabled.
| Type | Default | 🍎 | 🤖 | 🌐 |
| -------- | ------- | -- | -- | -- |
| `number` | `16` | | | ✅ |
## `style` [#style]
The sheet's content style override.
Content lays out naturally by default — like a regular view or a screen. Pass `flex: 1` to fill the sheet's visible height per detent.
| Type | Default | 🍎 | 🤖 | 🌐 |
| ---------------------- | ------- | -- | -- | -- |
| `StyleProp` | | ✅ | ✅ | ✅ |
# Lifecycle Events (/next/reference/events)
Lifecycle events available for `TrueSheet`.
```tsx
const App = () => {
const handleDismiss = () => {
console.log('Bye bye')
}
const handleOnWillPresent = (e: WillPresentEvent) => {
console.log(e.nativeEvent) // { index: 0, position: 123.5, detent: 0.5 }
console.log('Sheet is about to be presented')
}
const handleOnDidPresent = (e: DidPresentEvent) => {
console.log(e.nativeEvent) // { index: 0, position: 123.5, detent: 0.5 }
}
const handleDetentChange = (e: DetentChangeEvent) => {
console.log(e.nativeEvent) // { index: 1, position: 234.5, detent: 0.8 }
}
return (
sheet.current?.present()} />
sheet.current?.resize(1)} />
sheet.current?.dismiss(1)} />
)
}
```
## `onMount` [#onmount]
Called when the sheet's content is mounted and ready.
The sheet automatically waits for this event before presenting.
## `onWillPresent` [#onwillpresent]
Comes with [`DetentInfoEventPayload`](types#detentinfoeventpayload).
This is called when the sheet is about to be presented.
## `onDidPresent` [#ondidpresent]
Comes with [`DetentInfoEventPayload`](types#detentinfoeventpayload).
This is called when the sheet has been presented, providing the detent `index` and `position`.
## `onWillDismiss` [#onwilldismiss]
This is called when the sheet is about to be dismissed.
## `onDidDismiss` [#ondiddismiss]
This is called when the sheet has been dismissed.
## `onDismissAttempt` [#ondismissattempt]
This is called when the user tries to dismiss the sheet while [`dismissible`](configuration#dismissible) is `false` — by dragging it down, pressing the hardware back button (Android) or escape key (Web), or using an accessibility dismiss action. Tapping the dimmed background does not count, matching iOS. The sheet stays presented.
Use this to confirm destructive dismissals, then call [`dismiss`](methods#dismiss) to proceed. This follows Apple's `isModalInPresentation` + `presentationControllerDidAttemptToDismiss` pattern for unsaved changes.
```tsx
{
Alert.alert('Discard changes?', undefined, [
{ text: 'Keep editing', style: 'cancel' },
{ text: 'Discard', style: 'destructive', onPress: () => sheet.current?.dismiss() },
])
}}
>
{/* form */}
```
On Android, providing `onDismissAttempt` consumes the back press while `dismissible` is `false`. Return `false` from [`onBackPress`](#onbackpress) to let it propagate anyway.
## `onDetentChange` [#ondetentchange]
Comes with [`DetentInfoEventPayload`](types#detentinfoeventpayload).
This is called when the detent of the sheet has changed, either by dragging or by programmatically [resizing](methods#resize) it.
## `onDragBegin` [#ondragbegin]
Comes with [`DetentInfoEventPayload`](types#detentinfoeventpayload).
This is called when the sheet has began dragging.
## `onDragChange` [#ondragchange]
Comes with [`DetentInfoEventPayload`](types#detentinfoeventpayload).
This is called when the sheet is being dragged.
## `onDragEnd` [#ondragend]
Comes with [`DetentInfoEventPayload`](types#detentinfoeventpayload).
This is called when the sheet dragging has ended.
## `onPositionChange` [#onpositionchange]
Comes with [`PositionChangeEventPayload`](types#positionchangeeventpayload).
This is called continuously when the sheet's position changes during drag operations or transitions. This event fires more frequently than `onDragChange` and provides real-time position updates.
The `realtime` property indicates whether the position value is real-time (e.g., during drag or animation tracking). When `realtime` is `false`, the position should be animated in JS (ReanimatedTrueSheet handles this automatically).
Use this event when you need smooth, continuous position tracking for animations or visual feedback. For less frequent updates during dragging, use `onDragChange` instead.
## `onWillFocus` [#onwillfocus]
This is called when the sheet is about to gain focus. This happens:
* When the sheet is being presented (along with `onWillPresent`)
* When another sheet presented on top of it begins dismissing
## `onDidFocus` [#ondidfocus]
This is called when the sheet has gained focus. This happens:
* When the sheet has been presented (along with `onDidPresent`)
* When another sheet presented on top of it is dismissed
Useful for refreshing data or re-enabling interactions.
## `onWillBlur` [#onwillblur]
This is called when the sheet is about to lose focus. This happens:
* When the sheet is being dismissed (along with `onWillDismiss`)
* When another sheet is about to be presented on top of it
## `onDidBlur` [#ondidblur]
This is called when the sheet has lost focus. This happens:
* When the sheet has been dismissed (along with `onDidDismiss`)
* When another sheet is presented on top of it
Useful for pausing updates or disabling interactions.
## `onBackPress` [#onbackpress]
Android only
This is called when the hardware back button is pressed on Android. Use this to handle custom back press behavior, such as dismissing the sheet programmatically when `dismissible` is `false`.
# Imperative Methods (/next/reference/methods)
## Component Methods [#component-methods]
`TrueSheet` has a couple of core methods that you'll be using most of the time. These methods are asynchronous so you can use `async` and `await` on them.
Define the sheet and provide a `ref`. We will be using the `sheet` to access these methods.
```tsx {4}
const sheet = useRef(null)
return (
)
```
### `present` [#present]
Present the sheet. Optionally accepts a detent `index` and `animated` flag. See [`detents`](configuration#detents) prop.
| Parameters | Required | Default |
| ------------------- | -------- | ------- |
| `index: number` | No | `0` |
| `animated: boolean` | No | `true` |
```tsx
await sheet.current?.present()
// Present without animation
await sheet.current?.present(0, false)
```
### `dismiss` [#dismiss]
Dismisses the sheet and all sheets presented on top of it in a single animation. Optionally accepts an `animated` flag.
| Parameters | Required | Default |
| ------------------- | -------- | ------- |
| `animated: boolean` | No | `true` |
```tsx
await sheet.current?.dismiss()
// Dismiss without animation
await sheet.current?.dismiss(false)
```
### `dismissStack` [#dismissstack]
Dismisses only the sheets presented on top of this sheet, keeping this sheet presented. If no sheets are presented on top, this method does nothing.
| Parameters | Required | Default |
| ------------------- | -------- | ------- |
| `animated: boolean` | No | `true` |
```tsx
await sheet.current?.dismissStack()
// Dismiss stack without animation
await sheet.current?.dismissStack(false)
```
This is useful when you want to close child sheets while keeping the current sheet open.
### `resize` [#resize]
Resizes the sheet programmatically by `index`. The sheet must be presented before calling this method.
| Parameters | Required |
| --------------- | -------- |
| `index: number` | Yes |
```tsx
// Resize to 80%
await sheet.current?.resize(1)
```
## Global Methods [#global-methods]
You can also call the above methods statically without having access to a sheet's `ref`. This is particularly useful when you want to present a sheet from anywhere.
The API is similar to the component methods except for the required [`name`](configuration#name) prop. See [this guide](../guides/global-methods) for example.
In the example below, we provide `"my-sheet"` as the `name` of our sheet.
```tsx {4}
const sheet = useRef(null)
return (
)
```
You can then use these methods globally.
```tsx
await TrueSheet.present('my-sheet')
// Present without animation
await TrueSheet.present('my-sheet', 0, false)
```
```tsx
await TrueSheet.dismiss('my-sheet')
// Dismiss without animation
await TrueSheet.dismiss('my-sheet', false)
```
```tsx
await TrueSheet.dismissStack('my-sheet')
// Dismiss stack without animation
await TrueSheet.dismissStack('my-sheet', false)
```
If no sheets are presented on top, `dismissStack()` does nothing.
```tsx
// Resize to 80%
await TrueSheet.resize('my-sheet', 1)
```
### `dismissAll` [#dismissall]
Dismisses all presented sheets in the current stack. This is useful when you need to close all sheets at once, for example when navigating away or resetting the UI state.
| Parameters | Required | Default |
| ------------------- | -------- | ------- |
| `animated: boolean` | No | `true` |
```tsx
await TrueSheet.dismissAll()
// Dismiss all without animation
await TrueSheet.dismissAll(false)
```
This only dismisses sheets in the current presentation context. Sheets presented behind a modal (e.g., React Navigation modal or React Native Modal) will not be affected.
### Web [#web]
Static methods are not supported on web. Use the `useTrueSheet()` hook instead.
```tsx
import { useTrueSheet } from '@lodev09/react-native-true-sheet'
const { present, dismiss, dismissStack, resize, dismissAll } = useTrueSheet()
await present('my-sheet')
await dismiss('my-sheet')
await dismissAll()
```
# Types (/next/reference/types)
## `SheetDetent` [#sheetdetent]
```tsx
```
| Value | Description | 🍎 | 🤖 | 🌐 |
| -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------- | -- | -- |
| `"auto"` | Auto resize based on content height. | ***16+*** | ✅ | ✅ |
| `"peek"` | Collapsed height based on the combined [`header`](configuration#header) and absolute [`footer`](configuration#footer) heights, plus the content up to the bottom of a [`TrueSheetPeek`](../guides/peeking) component rendered within it. Falls back to a fixed height of `150` when none is provided. | ***16+*** | ✅ | ✅ |
| `number` | Fractional height (0-1) representing percentage of screen height. | ✅ | ✅ | ✅ |
The `"auto"` detent derives the sheet height from the content's natural height. With scrolling content, it sizes to the scroll view's content height and resizes as content grows or shrinks. See [Scrolling Content](../guides/scrolling).
## `DetentBackground` [#detentbackground]
An entry in [`detentBackgrounds`](configuration#detentbackgrounds). A string is a color value.
Object entries accept either color or blur, never both.
```tsx
export type DetentBackground =
| string
| { color: ColorValue; blur?: never }
| { blur: BackgroundBlur; color?: never };
```
`ColorValue` is a React Native color. [`BackgroundBlur`](#backgroundblur) selects an iOS blur style.
Android and Web inherit the sheet background for blur entries.
## `BackgroundBlur` [#backgroundblur]
Blur style that is mapped into native values in iOS.
```tsx
```
| Value |
| ------------------------------------ |
| `"light"` |
| `"dark"` |
| `"default"` |
| `"extra-light"` |
| `"regular"` |
| `"prominent"` |
| `"system-ultra-thin-material"` |
| `"system-thin-material"` |
| `"system-material"` |
| `"system-thick-material"` |
| `"system-chrome-material"` |
| `"system-ultra-thin-material-light"` |
| `"system-thin-material-light"` |
| `"system-material-light"` |
| `"system-thick-material-light"` |
| `"system-chrome-material-light"` |
| `"system-ultra-thin-material-dark"` |
| `"system-thin-material-dark"` |
| `"system-material-dark"` |
| `"system-thick-material-dark"` |
| `"system-chrome-material-dark"` |
## `ScrollableOptions` [#scrollableoptions]
Options for customizing scrollable behavior. Applies to the scrollable provided via [`scrollableRef`](configuration#scrollableref).
```tsx
```
| Property | Type | Description | Default |
| ------------------------ | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------- |
| `contentInsetAdjustment` | [`ContentInsetAdjustment`](#contentinsetadjustment) | What the sheet insets the bottom of the scroll content for: the bottom safe-area inset while the content can scroll, and/or the height of an `absolute` [`footer`](configuration#footer) floating over it (which then counts toward the `auto` detent). See [Bottom Safe-Area Inset](../guides/scrolling#bottom-safe-area-inset) and [Footer Inset](../guides/scrolling#footer-inset). | `'automatic'` |
| `scrollingExpandsSheet` | `boolean` | When `false`, scrolling the content does not expand the sheet to the next detent. Only dragging the grabber or sheet background expands it. Useful for YouTube-style comments sheets. | `true` |
| `keyboardScrollOffset` | `number` | Additional offset when scrolling to a focused input above the keyboard. | `0` |
| `keyboardOffset` | `number` | Adjusts the bottom inset applied to the scrollable when the keyboard is shown. Negative values reduce it — pass `-insets.bottom` to cancel out safe-area padding already included in the content's `paddingBottom`. | `0` |
| `topScrollEdgeEffect` | [`ScrollEdgeEffect`](#scrolledgeeffect) | The scroll edge effect applied to the top edge of the scroll view and header. iOS 26+ only. | `'hidden'` |
| `bottomScrollEdgeEffect` | [`ScrollEdgeEffect`](#scrolledgeeffect) | The scroll edge effect applied to the bottom edge of the scroll view and footer. iOS 26+ only. | `'hidden'` |
## `HeaderOptions` [#headeroptions]
Options for customizing header behavior.
```tsx
}
headerOptions={{
position: 'absolute',
}}
>
{/* ... */}
```
| Property | Type | Description | Default |
| ---------- | -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------ |
| `position` | `'relative' \| 'absolute'` | How the header participates in the sheet layout. `'relative'` takes up space above the content and is included in the `auto` detent height. `'absolute'` floats over the content, pinned to the top edge, and is excluded from the `auto` detent height. | `'relative'` |
## `FooterOptions` [#footeroptions]
Options for customizing footer behavior.
```tsx
}
footerOptions={{
position: 'absolute',
keyboardOffset: -16,
}}
>
{/* ... */}
```
| Property | Type | Description | Default |
| ---------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------ |
| `position` | `'relative' \| 'absolute'` | How the footer participates in the sheet layout. `'relative'` takes up space below the content, pinned to the bottom edge — included in the `auto` detent height but excluded from the `peek` detent height (it's pushed off-screen at peek). `'absolute'` floats over the content, pinned to the bottom edge — excluded from the `auto` detent height but included in the `peek` detent height. | `'relative'` |
| `keyboardOffset` | `number` | Adjusts how far an `absolute` footer rises when the keyboard opens. Positive values raise it higher; negative values tuck the footer's own bottom padding behind the keyboard instead of leaving a gap. The built-in safe-area inset is skipped automatically while the keyboard is open. A `relative` footer stays in the layout flow behind the keyboard, so this has no effect. | `0` |
| `avoidKeyboard` | `boolean` | Whether an `absolute` footer rises above the keyboard. Set to `false` to keep it at the bottom edge of the sheet. With footer inset adjustment enabled, any footer portion above the keyboard still adds scroll padding and contributes to the expanded `auto` height. The pinned footer keeps its safe-area padding and ignores `keyboardOffset`. No effect on relative footers. Native only. | `true` |
## `ContentInsetAdjustment` [#contentinsetadjustment]
What the sheet insets the bottom of a plugged scrollable's content for. Set via [`ScrollableOptions`](#scrollableoptions).
| Value | Description |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `'automatic'` | The bottom safe-area inset while the content can scroll, plus the height of an `absolute` footer floating over the scroll view — the footer then counts toward the `auto` detent. This is the default. |
| `'safe-area'` | The safe-area inset only — content scrolls under an absolute footer. |
| `'footer'` | The absolute footer only — you pad for the safe area. |
| `'never'` | No inset — you pad the content yourself. |
The safe-area inset only applies while the sheet's [`insetAdjustment`](configuration#insetadjustment) is `'automatic'`; a relative footer absorbs it instead. The footer inset is independent of `insetAdjustment` — only the part of the footer overlapping the scroll view is applied, and it stacks with the keyboard inset. See [Scrolling Content](../guides/scrolling#bottom-safe-area-inset).
## `ScrollEdgeEffect` [#scrolledgeeffect]
Controls the blur/gradient edge effect on the scroll view edges and header/footer overlay views. iOS 26+ only.
| Value | Description |
| ------------- | ------------------------------------ |
| `'automatic'` | System default edge effect style. |
| `'hard'` | A hard, opaque edge effect. |
| `'soft'` | A soft, gradient edge effect. |
| `'hidden'` | No edge effect. This is the default. |
## `GrabberOptions` [#grabberoptions]
Options for customizing the grabber (drag handle) appearance.
```tsx
```
| Property | Type | Description | Default |
| -------------- | ------------ | ------------------------------------------------------------------------------------------------------ | ------------------------ |
| `width` | `number` | The width of the grabber pill. | iOS: `36`, Android: `32` |
| `height` | `number` | The height of the grabber pill. | iOS: `5`, Android: `4` |
| `topMargin` | `number` | The top margin from the sheet edge. | iOS: `5`, Android: `16` |
| `cornerRadius` | `number` | The corner radius of the grabber pill. | `height / 2` |
| `color` | `ColorValue` | The color of the grabber. Uses native styling when not provided. | |
| `adaptive` | `boolean` | Whether the grabber adapts to light/dark mode. Uses vibrancy on iOS and theme-based colors on Android. | `true` |
## `AccessibilityOptions` [#accessibilityoptions]
Options for customizing (e.g. localizing) the accessibility strings announced by screen readers.
```tsx
```
| Property | Type | Description | Default |
| --------------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------- |
| `grabberLabel` | `string` | The accessibility label of the grabber. | iOS: `'Sheet Grabber'`, Android: `'Drag handle'` |
| `grabberHint` | `string` | The accessibility hint of the grabber. iOS only. | `'Double-tap to expand. Swipe up or down to resize the sheet'` |
| `expandedValue` | `string` | The value announced when the sheet is at the last detent. | `'Expanded'` |
| `collapsedValue` | `string` | The value announced when the sheet is at the first detent. | `'Collapsed'` |
| `detentValue` | `string` | The value announced at intermediate detents. Supports `{index}` and `{count}` placeholders. | `'Detent {index} of {count}'` |
| `expandActionLabel` | `string` | The label of the expand accessibility action. Android only. | `'Expand'` |
| `collapseActionLabel` | `string` | The label of the collapse accessibility action. Android only. | `'Collapse'` |
| `paneTitle` | `string` | The title announced when the sheet appears. Sets the accessibility pane title on Android and the hidden dialog title on Web. | Android: `'Bottom sheet'`, Web: `'Sheet'` |
On iOS, the grabber strings only apply when [`grabberOptions`](configuration#grabberoptions) is provided. Without it, iOS uses the system grabber which is already localized by the system.
## `InsetAdjustment` [#insetadjustment]
Controls how the bottom safe area inset affects detent heights.
```tsx
```
| Value | Description |
| ------------- | ------------------------------------------------------------------ |
| `"automatic"` | System handles insets automatically. This is the default behavior. |
| `"never"` | TrueSheet will keep the layout as-is for precise sizing. |
## `DismissThreshold` [#dismissthreshold]
How far the sheet must be swiped down from its lowest detent to dismiss.
```tsx
```
| Value | Description |
| --------- | ---------------------------------------------------------------------------------------------------------------- |
| `"half"` | Dismisses past half of the lowest detent, or on a quick flick. Matches iOS. This is the default behavior. |
| `"short"` | Dismisses on a short drag (56 dp) or any downward fling (125 dp/s), like Compose Material3's `ModalBottomSheet`. |
## `DetentInfoEventPayload` [#detentinfoeventpayload]
`Object` that comes with most sheet events.
```tsx
{
index: 1,
position: 123.5,
detent: 0.5
}
```
| Property | Type | Description |
| -------- | -------- | --------------------------------------------------------------- |
| index | `number` | The detent index from the provided detents. See `detents` prop. |
| position | `number` | The Y position of the sheet relative to the screen. |
| detent | `number` | The detent value (0-1) for the current index. |
## `PositionChangeEventPayload` [#positionchangeeventpayload]
`Object` that comes with the `onPositionChange` event. Extends [`DetentInfoEventPayload`](#detentinfoeventpayload).
```tsx
{
index: 1.5,
position: 123.5,
detent: 0.5,
realtime: true
}
```
| Property | Type | Description |
| -------- | --------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| index | `number` | The interpolated detent index. Continuous value that smoothly transitions between detents (e.g., `0.5` means halfway between detent 0 and 1). |
| position | `number` | The Y position of the sheet relative to the screen. |
| detent | `number` | The detent value (0-1) for the nearest detent index. |
| realtime | `boolean` | Whether the position is a real-time value (e.g., during drag or animation tracking). When `false`, position should be animated in JS. |
# Backgrounds (/next/guides/backgrounds)
A sheet background has two layers:
| Layer | Props | Renders |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------- |
| Sheet background | [`backgroundColor`](../reference/configuration#backgroundcolor), [`backgroundBlur`](../reference/configuration#backgroundblur), [`detentBackgrounds`](../reference/configuration#detentbackgrounds) | Native effect or painted color |
| Background wrapper | [`background`](../reference/configuration#background), [`backgroundStyle`](../reference/configuration#backgroundstyle) | React view above the sheet background |
The wrapper fills the sheet behind the header, content, and footer. It ignores touches and applies across all detents.
It does not affect content height or detent calculations.
## Background color [#background-color]
Set [`backgroundColor`](../reference/configuration#backgroundcolor) to replace the default background.
```tsx
{/* Sheet content */}
```
Without a color or blur, iOS 26+ uses [Liquid Glass](liquid-glass), while earlier iOS versions use system-material blur.
Android and Web use Material Design 3's `colorSurfaceContainerLow`, which adapts to light/dark mode.
On supported iOS 26.1+ devices, `backgroundColor` uses a native color effect. It replaces Liquid Glass instead of tinting it.
Use `backgroundColor="transparent"` to remove the glass without adding a color.
## Blur [#blur]
Set [`backgroundBlur`](../reference/configuration#backgroundblur) for a system blur on iOS.
```tsx
{/* Sheet content */}
```
Blur takes precedence over `backgroundColor`. The built-in blur uses system intensity and ignores touches.
For custom intensity or blur on Android and Web, use a [custom blur](#custom-blur).
## Tinting [#tinting]
[`backgroundStyle`](../reference/configuration#backgroundstyle) paints over the glass or blur. Only `backgroundColor` is supported.
A translucent color tints the effect and keeps the content behind the sheet visible.
```tsx
// Tinted Liquid Glass
// Tinted blur
```
## Exact colors [#exact-colors]
The native color effect can shift dark, low-chroma colors over full-screen modals.
Use an opaque `backgroundStyle.backgroundColor` for an exact color:
```tsx
```
## Custom background [#custom-background]
Pass a `ReactNode` to [`background`](../reference/configuration#background) to render a gradient, image, or any other view.
Size the element with `StyleSheet.absoluteFill`.
```tsx
}
>
{/* Sheet content */}
```
### Custom blur [#custom-blur]
Use a third-party blur for custom intensity. The blur library controls intensity and platform support.
For example, with `expo-blur` installed:
```tsx
import { BlurView } from 'expo-blur';
import { StyleSheet } from 'react-native';
import { TrueSheet } from '@lodev09/react-native-true-sheet';
}
>
{/* Sheet content */}
```
The custom background sits above the sheet background effects. On iOS 26.0 or devices without the setters, a transparent color cannot remove the system glass.
## Per-detent backgrounds [#per-detent-backgrounds]
[`detentBackgrounds`](../reference/configuration#detentbackgrounds) selects a color or blur for each detent.
```tsx
```
In this example, the peek detent uses the sheet background. The other detents use blur and color.
A `null` or missing entry inherits `backgroundColor`/`backgroundBlur`. An entry overrides it.
| Platform | Transition | Blur entries |
| ------------------------------------ | ----------------------------------------------------------- | ----------------------------- |
| iOS 26.1+ with native effect support | UIKit transitions as the sheet moves | Native blur effect |
| Other iOS devices | Cross-fade after passing the midpoint between detents | Blur view at system intensity |
| Android and Web | Color cross-fade after passing the midpoint between detents | Inherit the sheet background |
On iOS 26+, detents with a color or blur entry lose the glass touch response.
Tinted glass effects are not supported per detent. `backgroundStyle` can tint the background, but its tint applies across all detents.
An opaque background wrapper hides the detent backgrounds.
## iOS background effects [#ios-background-effects]
Native effects require iOS 26.1+, the background effect setters on the sheet and its detents, and no design compatibility mode.
Design compatibility mode applies only on iOS 26. iOS 27+ ignores `UIDesignRequiresCompatibility`.
| Input | Native effects | Fallback |
| --------------------------------- | ---------------------------- | ---------------------------------------------------- |
| `backgroundColor` | Color effect | Painted color |
| `backgroundBlur` | Blur effect, replaces glass | Non-interactive blur view at system intensity |
| Neither | Liquid Glass | Platform default; system-material blur before iOS 26 |
| `backgroundStyle.backgroundColor` | Paint above the effect | Paint above the sheet background |
| `background` | Custom node above the effect | Custom node above the sheet background |
The fallback covers older iOS versions, devices without the setters, and `UIDesignRequiresCompatibility` mode.
On iOS 26.0, the fallback color or blur sits above Liquid Glass, which TrueSheet cannot remove.
## Platform support [#platform-support]
| Prop | 🍎 | 🤖 | 🌐 |
| -------------------------------- | -- | ----------- | ----------- |
| `backgroundColor` | ✅ | ✅ | ✅ |
| `backgroundBlur` | ✅ | | |
| `detentBackgrounds` | ✅ | Colors only | Colors only |
| `background` / `backgroundStyle` | ✅ | ✅ | ✅ |
# Deep Linking (/next/guides/deep-linking)
A screen can [present its sheet on mount](onmount) with `initialDetentIndex`. When a deep link opens the app from a cold start, another screen can open on top of that screen at the same time — a modal, or a sheet in a [sheet navigator](navigation).
## The Problem [#the-problem]
On iOS, the `initialDetentIndex` sheet presents while the deep-linked screen is still opening. It ends up on top of the deep-linked screen, or doesn't present at all.
## Present When Focused [#present-when-focused]
Hold `initialDetentIndex` at `-1` until the screen is focused:
```tsx {5-11,14}
import { useFocusEffect } from '@react-navigation/native'; // or 'expo-router'
import { useCallback, useState } from 'react';
function HomeScreen() {
const [initialDetentIndex, setInitialDetentIndex] = useState(-1);
useFocusEffect(
useCallback(() => {
setInitialDetentIndex(0);
}, [])
);
return (
{/* ... */}
);
}
```
On a normal launch the screen is focused on mount, so the sheet presents right away. When a deep link opens another screen on top, the screen isn't focused, so the sheet waits until that screen closes.
Flip `initialDetentIndex` instead of calling `present()` on focus. A screen behind a `fullScreenModal` gains focus before it's back in the window, so `present()` rejects with "No presenting view controller found".
## Example [#example]
The [repro routes](https://github.com/lodev09/react-native-true-sheet/tree/main/example/expo/app/repro) in the Expo example use this pattern. They're only reachable by deep link — terminate the app, then open one:
```sh
yarn expo open repro/modal # fullScreenModal
yarn expo open repro/page-sheet # pageSheet modal
yarn expo open repro/sheet/details # sheet navigator
```
# Dimming (/next/guides/dimming)
One of the most common use cases for a Bottom Sheet is to present it while still allowing users to interact with background components, such as in a Maps app.
In this guide, you can configure `TrueSheet` to achieve this exact functionality.
## How? [#how]
You can easily disable the dimmed background of the sheet by setting [`dimmed`](../reference/configuration#dimmed) to `false`.
```tsx {5}
export const App = () => {
return (
)
}
```
### Dimmed by Detent Index [#dimmed-by-detent-index]
To further customize the dimming behavior, [`dimmedDetentIndex`](../reference/configuration#dimmeddetentindex) is also available. Set the [detent](../reference/configuration#detents) `index` at which you want the sheet to start dimming.
```tsx {5}
export const App = () => {
return (
)
}
```
`dimmedDetentIndex` is ignored if `dimmed` is set to `false`.
## Dim Color and Opacity [#dim-color-and-opacity]
Use [`dimColor`](../reference/configuration#dimcolor) and [`dimOpacity`](../reference/configuration#dimopacity) to change the dim's color and how strong it is.
```tsx {5-6}
export const App = () => {
return (
)
}
```
`dimOpacity={0}` hides the dim, but it still blocks touches to the background and a tap still dismisses the sheet. Set `dimmed={false}` to let touches through.
On iOS, setting `dimColor` or `dimOpacity` replaces the system dim with a custom dim layer. Stacked sheets each add their own layer, so the background gets darker with every sheet.
## Customizing Dimming Alpha [#customizing-dimming-alpha]
You can dynamically control the dimming opacity based on the sheet's position using `ReanimatedTrueSheet` with `animatedPosition`.
Learn more about Reanimated integration in the [Reanimated guide](reanimated).
```tsx {2-3,7,9-11,14-21}
import { ReanimatedTrueSheet, ReanimatedTrueSheetProvider, useReanimatedTrueSheet } from '@lodev09/react-native-true-sheet/reanimated'
import Animated, { useAnimatedStyle } from 'react-native-reanimated'
import { StyleSheet } from 'react-native'
export const App = () => {
return (
)
}
const CustomBackdrop = () => {
const { animatedPosition } = useReanimatedTrueSheet()
const backdropStyle = useAnimatedStyle(() => ({
opacity: animatedPosition.value * 0.5, // Adjust multiplier for desired alpha
}))
return
}
const Sheet = () => {
return (
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
backdrop: {
backgroundColor: 'black',
},
})
```
This allows you to create custom dimming effects that respond to the sheet's movement in real-time.
# Edge-to-Edge (/next/guides/edge-to-edge)
TrueSheet automatically detects and adapts to Android's edge-to-edge display mode, providing a modern, immersive experience.
## Enabling Edge-to-Edge [#enabling-edge-to-edge]
Edge-to-edge is supported in React Native 0.81+ via the `edgeToEdgeEnabled` Gradle property.
Enable it in your `android/gradle.properties`:
```groovy title="android/gradle.properties"
edgeToEdgeEnabled=true
```
This property is **disabled by default** for current Android versions. TrueSheet will auto-detect when you enable it.
Starting with **Android 16+ (API level 36)**, edge-to-edge will be [automatically enabled by default](https://developer.android.com/about/versions/16/behavior-changes-16#edge-to-edge) by the system.
**Learn more:**
* [React Native 0.81 edge-to-edge announcement](https://reactnative.dev/blog/2025/08/12/react-native-0.81)
* [Android 16 edge-to-edge enforcement](https://developer.android.com/about/versions/16/behavior-changes-16#edge-to-edge)
## Default Behavior [#default-behavior]
By default, when edge-to-edge is enabled, the sheet respects the status bar and stops at the bottom of it when fully expanded. This ensures content remains visible and not obscured by the status bar.
```tsx
```
TrueSheet works seamlessly with edge-to-edge enabled and provides automatic status bar detection.
# Adding Footer (/next/guides/footer)
Do you need action buttons on the sheet? Or perhaps a small banner at the bottom? We've got you covered!
`TrueSheet` provides first-class support for a `Footer` component. The footer is pinned to the bottom of the sheet, takes up space below the content, and its height is included in the [`auto`](../reference/types#sheetdetent) detent calculation.
## How? [#how]
Define your footer component.
```tsx
const SomeFooter = () => {
return (
My Foot-er is Awesome.
)
}
```
Stick it in the [`footer`](../reference/configuration#footer).
```tsx {3}
const App = () => {
return (
)
}
```
For best performance, use `ReactElement` instead!
```tsx {5-9}
const App = () => {
return (
My Foot-er is more awesome.
}
>
)
}
```
## Safe Area Handling [#safe-area-handling]
The footer owns the sheet's bottom edge, so it automatically absorbs the bottom safe-area inset as padding when [`insetAdjustment`](../reference/configuration#insetadjustment) is `"automatic"` (the default). Your footer content stays above the home indicator while its background fills the inset — no manual padding needed.
```tsx
const MyFooter = () => {
return (
Footer Content
);
};
// Usage
} footerStyle={{ backgroundColor: '#333' }}>
{/* content */}
```
The inset is applied to the footer component itself, so set your background via [`footerStyle`](../reference/configuration#footerstyle) (or on your footer's root view) for it to fill the safe area.
When the keyboard opens, an absolute footer rises above it without a safe-area gap. With `footerOptions.avoidKeyboard: false`, it stays behind the keyboard and keeps its inset. A relative footer also keeps its inset. See [Keyboard Handling](./keyboard#footer-behavior).
On iPad, the sheet is displayed as a floating modal, so no inset is applied. Set `insetAdjustment="never"` to opt out entirely.
## Floating Footer [#floating-footer]
By default, the footer takes up space below the content and its height is included in the [`auto`](../reference/types#sheetdetent) detent calculation. Set `footerOptions.position` to `'absolute'` to float the footer over the content instead — it stays pinned to the bottom edge but no longer adds to the `auto` detent height. An absolute footer absorbs the bottom safe-area inset the same way a relative one does — see [Safe Area Handling](#safe-area-handling).
```tsx {5-6}
const App = () => {
return (
}
>
} />
)
}
```
Since the footer overlaps the content, a plugged [`scrollableRef`](../reference/configuration#scrollableref) is padded by the footer's measured height automatically — the last item scrolls into view above the footer, and the footer counts toward the `auto` detent. See [Footer Inset](scrolling#footer-inset). For non-scrolling content, add bottom padding yourself so it ends above the footer.
A relative footer is laid out below the content — if your content is taller than the sheet's visible height (e.g. fixed detents), bound it with `flex: 1` so the footer stays visible. Use `'absolute'` if you want the footer to float regardless of the content's height.
## Collapsing to the Footer [#collapsing-to-the-footer]
An **absolute** footer's height counts toward the [`"peek"`](../reference/types#sheetdetent) detent — collapse the sheet down to just the [`header`](header), footer, and any peeking content.
```tsx {4-6}
const App = () => {
return (
}
footerOptions={{ position: 'absolute' }}
>
)
}
```
Since an absolute footer floats over the content, it stays visible at every detent. A relative footer is laid out below the content, so it's pushed off-screen at the peek detent and is excluded from the peek height. See [Peeking Content](peeking) for the full guide.
# Global Methods (/next/guides/global-methods)
`TrueSheet` relies heavily on [component methods](../reference/methods) to present and dismiss the sheet. However, this becomes challenging when the button is deep within the component tree.
To resolve this issue, `TrueSheet` provides [global methods](../reference/methods#global-methods) that allow you to present or dismiss any sheet from anywhere!
## How? [#how]
Somewhere in your App, define the sheet with a [`name`](../reference/configuration#name).
```tsx {3}
const App = () => {
return (
)
}
```
In your component, present the sheet using the `present` global method.
```tsx {5}
import { TrueSheet } from '@lodev09/react-native-true-sheet'
const SomeComponent = () => {
const presentMySheet = () => {
TrueSheet.present('my-sheet') // 🎉
}
return (
)
}
```
Guess what? You can also `dismiss` it globally 🤩.
```tsx
TrueSheet.dismiss('my-sheet') // ✅
```
# Adding Header (/next/guides/header)
Need a fixed header above your scrollable content? A title bar, search input, or navigation controls? TrueSheet makes it simple with the `header` prop!
## Using the `header` Prop [#using-the-header-prop]
The recommended way to add a header is using the `header` prop. This creates a native header view that is properly accounted for in layout calculations, ensuring your scrollable content gets the correct available height.
```tsx {4-8}
const App = () => {
return (
My Header
}
>
{/* Your scrollable content */}
)
}
const styles = StyleSheet.create({
header: {
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
})
```
## With Search Input [#with-search-input]
A common use case is adding a search bar in the header:
```tsx {4-8}
const App = () => {
return (
}
>
}
/>
)
}
```
When using `header` with `FlatList` or `ScrollView`, the content area height is automatically adjusted to account for the header height. This ensures proper scrolling behavior on both iOS and Android.
## Floating Header [#floating-header]
By default, the header takes up space above the content and its height is included in the [`auto`](../reference/types#sheetdetent) detent calculation. Set `headerOptions.position` to `'absolute'` to float the header over the content instead — it gets pinned to the top edge and no longer adds to the `auto` detent height.
```tsx {5-5}
const App = () => {
return (
}
>
}
/>
)
}
```
Since the header overlaps the content, add top padding to your content (e.g. `contentContainerStyle`) so it starts below the header.
## Collapsing to the Header [#collapsing-to-the-header]
Use the [`"peek"`](../reference/types#sheetdetent) detent to collapse the sheet down to just the header (plus an [absolute `footer`](footer#floating-footer), if provided). Great for map-style sheets with a compact summary that expands into full content.
```tsx {4-4}
const App = () => {
return (
}
footer={}
footerOptions={{ position: 'absolute' }}
>
)
}
```
The peek height automatically follows the measured header and footer sizes — no `onLayout` juggling needed. You can also include part of the content via the `TrueSheetPeek` component — see [Peeking Content](peeking) for the full guide.
## Platform Support [#platform-support]
The `header` prop is supported on both **iOS** and **Android**.
# Testing with Jest (/next/guides/jest)
Testing components that use `TrueSheet` is straightforward with the built-in Jest mocks.
## Setup [#setup]
Add the mocks to your Jest setup file:
```js
// jest.setup.js
// Main component
jest.mock('@lodev09/react-native-true-sheet', () =>
require('@lodev09/react-native-true-sheet/mock')
);
// Navigation (if using)
jest.mock('@lodev09/react-native-true-sheet/navigation', () =>
require('@lodev09/react-native-true-sheet/navigation/mock')
);
// Expo Router navigation (if using)
jest.mock('@lodev09/react-native-true-sheet/navigation/expo-router', () =>
require('@lodev09/react-native-true-sheet/navigation/expo-router/mock')
);
// Reanimated (if using)
jest.mock('@lodev09/react-native-true-sheet/reanimated', () =>
require('@lodev09/react-native-true-sheet/reanimated/mock')
);
```
Configure Jest to use the setup file and transform the package:
```json
{
"jest": {
"setupFilesAfterEnv": ["/jest.setup.js"],
"transformIgnorePatterns": [
"node_modules/(?!(react-native|@react-native|@lodev09/react-native-true-sheet)/)"
]
}
}
```
The `transformIgnorePatterns` config is required because the mock files use ESM syntax. This tells Jest to transform the package instead of skipping it.
## Available Mocks [#available-mocks]
### Main Module (`/mock`) [#main-module-mock]
* `TrueSheet` - Component with mocked `present`, `dismiss`, `resize` methods
* `TrueSheetProvider` - Pass-through provider
* `useTrueSheet` - Hook returning mocked methods
### Navigation Module (`/navigation/mock`) [#navigation-module-navigationmock]
* `createTrueSheetNavigator` - Mocked navigator factory
* `createTrueSheetScreen` - Mocked static API screen factory
* `TrueSheetActions` - Mocked action creators
* `useTrueSheetNavigation` - Hook returning mocked navigation object
### Expo Router Module (`/navigation/expo-router/mock`) [#expo-router-module-navigationexpo-routermock]
* `Sheet` - Pass-through layout with mocked `Screen` and `Protected`
* `TrueSheetActions` - Mocked action creators
* `useTrueSheetNavigation` - Hook returning mocked navigation object
### Reanimated Module (`/reanimated/mock`) [#reanimated-module-reanimatedmock]
* `ReanimatedTrueSheet` - Component with mocked methods
* `ReanimatedTrueSheetProvider` - Pass-through provider
* `useReanimatedTrueSheet` - Hook returning mocked shared values
* `useReanimatedPositionChangeHandler` - Mocked handler hook
## Testing Static Methods [#testing-static-methods]
All static methods are mocked as Jest functions.
```tsx
import { TrueSheet } from '@lodev09/react-native-true-sheet';
it('should present sheet', async () => {
await TrueSheet.present('my-sheet', 0);
expect(TrueSheet.present).toHaveBeenCalledWith('my-sheet', 0);
});
it('should dismiss sheet', async () => {
await TrueSheet.dismiss('my-sheet');
expect(TrueSheet.dismiss).toHaveBeenCalledWith('my-sheet');
});
```
## Testing Component Rendering [#testing-component-rendering]
The mock renders `TrueSheet` as a View with all props passed through.
```tsx
it('should render sheet content', () => {
const { getByText } = render(
Sheet Content
);
expect(getByText('Sheet Content')).toBeDefined();
});
```
## Testing Reanimated Integration [#testing-reanimated-integration]
```tsx
import {
useReanimatedTrueSheet,
ReanimatedTrueSheetProvider,
} from '@lodev09/react-native-true-sheet/reanimated';
it('should return mocked shared values', () => {
const { result } = renderHook(() => useReanimatedTrueSheet(), {
wrapper: ReanimatedTrueSheetProvider,
});
expect(result.current.animatedPosition.value).toBe(0);
expect(result.current.animatedIndex.value).toBe(-1);
});
```
## Best Practices [#best-practices]
Clear mock calls between tests to avoid interference.
```tsx
describe('MyComponent', () => {
beforeEach(() => {
jest.clearAllMocks();
});
// Your tests...
});
```
All methods return resolved Promises, so remember to `await` them in your tests.
# Keyboard Handling (/next/guides/keyboard)
TrueSheet handles keyboard visibility natively on both iOS and Android. When a `TextInput` inside the sheet is focused, the sheet automatically adjusts to keep the input visible above the keyboard.
## How? [#how]
The keyboard handling is built into the native implementation:
* **iOS**: Uses `UISheetPresentationController`'s built-in keyboard avoidance
* **Android**: Tracks keyboard height via `WindowInsetsAnimationCompat` and reconfigures sheet detents in real-time
No additional configuration is required. Simply add your `TextInput` components inside the sheet:
```tsx
const App = () => {
return (
)
}
```
## Scrollable + Keyboard [#scrollable--keyboard]
When your sheet contains a `ScrollView` or `FlatList` (provided via [`scrollableRef`](../reference/configuration#scrollableref)), keyboard handling works out of the box. If a `TextInput` inside a `ScrollView` is focused, the sheet automatically scrolls to keep the input visible above the keyboard — no extra setup needed.
You can use `keyboardScrollOffset` in [`scrollableOptions`](../reference/configuration#scrollableoptions) to add extra spacing between the focused input and the keyboard:
```tsx
```
This is especially useful for forms with multiple inputs — as the user taps between fields, the scroll view adjusts automatically to keep the active input in view.
While the keyboard is open, the scrollable gets a bottom inset matching the keyboard height. If your content renders its own bottom padding (e.g. you opted out of [`contentInsetAdjustment`](../guides/scrolling#bottom-safe-area-inset) and applied safe-area padding via `paddingBottom`), that padding stacks on top of the inset and leaves a gap above the keyboard. Use `keyboardOffset` to adjust the inset — pass a negative value to cancel out the padding:
```tsx
const insets = useSafeAreaInsets()
```
## Footer Behavior [#footer-behavior]
A relative [`footer`](../reference/configuration#footer) (the default) stays in the layout flow — it doesn't float above the keyboard. The keyboard simply covers it, and it's revealed again when the keyboard hides.
An absolute footer automatically repositions above the keyboard, staying visible while the user types. The automatic scroll accounts for the footer's height — focused inputs clear both the keyboard and the footer. Its built-in [safe-area inset](./footer#safe-area-handling) is skipped while the keyboard is open — the footer hugs the keyboard with no gap, and the inset is restored when the keyboard hides.
If your absolute footer renders its own bottom padding, that padding leaves a gap above the keyboard. Use `keyboardOffset` in [`footerOptions`](../reference/configuration#footeroptions) to adjust the rise — pass a negative value to tuck it behind the keyboard:
```tsx
} footerOptions={{ position: 'absolute', keyboardOffset: -16 }}>
{/* ... */}
```
The footer slides up by the keyboard height plus `keyboardOffset`. A negative value reduces the rise so the padding sits behind the keyboard; a positive value raises the footer higher.
Set `avoidKeyboard` to `false` to keep an absolute footer at the bottom edge of the sheet:
```tsx
} footerOptions={{ position: 'absolute', avoidKeyboard: false }}>
{/* ... */}
```
The keyboard covers the pinned footer. With footer inset adjustment enabled, any footer portion above the keyboard still adds scroll padding and contributes to the expanded `auto` height. A fully covered footer adds no extra space above the keyboard. The footer keeps its safe-area padding, and `keyboardOffset` has no effect.
## Autofocus Limitation [#autofocus-limitation]
Avoid using `autoFocus` on `TextInput` components inside the sheet. The keyboard may appear before the sheet has finished presenting, causing layout issues.
Instead, focus the input programmatically after the sheet has presented:
```tsx {4,7}
const inputRef = useRef(null)
const handlePresent = () => {
inputRef.current?.focus()
}
```
# Liquid Glass (/next/guides/liquid-glass)
Starting with iOS 26, Apple introduced the **Liquid Glass** visual effect, a new design element that creates a frosted glass appearance on sheets and modals.
TrueSheet **supports Liquid Glass by default** on iOS 26+, giving your sheets a modern, native look.
## What is Liquid Glass? [#what-is-liquid-glass]
Liquid Glass is a visual effect introduced in iOS 26 that provides a translucent, frosted glass appearance with a subtle blur. It's part of Apple's latest design language and is automatically applied to native sheet presentations.
By default, TrueSheet enables Liquid Glass on iOS 26+ devices.
On supported iOS 26.1+ devices, `backgroundColor` or `backgroundBlur` replaces it with a native effect. With neither prop set, the default is Liquid Glass.
See the [Backgrounds guide](backgrounds) for colors, blur, custom backgrounds, per-detent backgrounds, and the iOS fallback behavior.
## Tinting Liquid Glass [#tinting-liquid-glass]
[`backgroundStyle`](../reference/configuration#backgroundstyle) paints over the glass or blur. A translucent color tints the effect and keeps the content behind the sheet visible.
```tsx
{/* Sheet content */}
```
Tinted glass is not supported per detent. On iOS 26+, detents with a [`detentBackgrounds`](backgrounds#per-detent-backgrounds) entry lose the glass touch response.
## Disabling Liquid Glass [#disabling-liquid-glass]
If you prefer the classic sheet appearance without Liquid Glass, there are two options:
### Using `backgroundColor` or `backgroundBlur` [#using-backgroundcolor-or-backgroundblur]
Setting [`backgroundColor`](../reference/configuration#backgroundcolor) or [`backgroundBlur`](../reference/configuration#backgroundblur) replaces glass with that effect.
```tsx
{/* Sheet content */}
```
```tsx
{/* Sheet content */}
```
Use `backgroundColor="transparent"` to remove the glass without adding a color. Leave `backgroundBlur` unset, since blur takes precedence.
```tsx
{/* Sheet content */}
```
This requires iOS 26.1+ with native background effect support. Devices without the setters use the [fallback](backgrounds#ios-background-effects).
### Using `UIDesignRequiresCompatibility` [#using-uidesignrequirescompatibility]
On iOS 26, set `UIDesignRequiresCompatibility` to `true` in your `Info.plist` to disable Liquid Glass.
iOS 27 and later ignore this key. Use `backgroundColor` or `backgroundBlur` to replace Liquid Glass on individual sheets.
#### Using Info.plist [#using-infoplist]
Add the following key to your `ios/YourApp/Info.plist`:
```xml title="ios/YourApp/Info.plist"
UIDesignRequiresCompatibility
```
#### Using Expo Config Plugin [#using-expo-config-plugin]
If you're using Expo, you can configure this through your `app.json` or `app.config.js`:
```js title="app.config.js"
export default {
expo: {
ios: {
infoPlist: {
UIDesignRequiresCompatibility: true,
},
},
},
}
```
After making this change, rebuild your app:
```sh
npx expo prebuild --clean
npx expo run:ios
```
On iOS 26, this setting disables Liquid Glass across the entire app. It has no effect on iOS 27+.
See [Apple's documentation](https://developer.apple.com/documentation/BundleResources/Information-Property-List/UIDesignRequiresCompatibility) for more details.
## Learn More [#learn-more]
* [How to create Apple Maps style liquid glass sheets in Expo](https://expo.dev/blog/how-to-create-apple-maps-style-liquid-glass-sheets) - Expo Blog
* [Apple's Liquid Glass Documentation](https://developer.apple.com/documentation/TechnologyOverviews/liquid-glass)
# Navigation (/next/guides/navigation)
TrueSheet integrates with React Navigation out of the box. It just works!
## How? [#how]
You can use the [Sheet Navigator](#sheet-navigator) to present screens as sheets (also works with [Expo Router](#expo-router)), or simply [navigate from within sheets](#navigating-from-sheets) using your existing navigation setup.
## Sheet Navigator [#sheet-navigator]
TrueSheet provides a custom navigator built on [`standard-navigation`](https://github.com/react-navigation/standard-navigation), so the same implementation works with both React Navigation and [Expo Router](#expo-router). The first screen (or `initialRouteName`) is the base content, while other screens are presented as sheets.
Install `@react-navigation/native` (version `7.3.0` or higher) and `standard-navigation`. Both are declared as optional peer dependencies.
npm
pnpm
yarn
bun
```bash
npm install @react-navigation/native standard-navigation
```
```bash
pnpm add @react-navigation/native standard-navigation
```
```bash
yarn add @react-navigation/native standard-navigation
```
```bash
bun add @react-navigation/native standard-navigation
```
### Basic Usage [#basic-usage]
```tsx
import { NavigationContainer } from '@react-navigation/native';
import {
createTrueSheetNavigator,
useTrueSheetNavigation,
} from '@lodev09/react-native-true-sheet/navigation';
const Sheet = createTrueSheetNavigator();
function App() {
return (
{/* Base screen (first screen is the default) */}
{/* Sheet screens */}
);
}
```
### Static API [#static-api]
The navigator also supports React Navigation's [static API](https://reactnavigation.org/docs/static-configuration) via `createTrueSheetScreen`:
```tsx
import {
createTrueSheetNavigator,
createTrueSheetScreen,
} from '@lodev09/react-native-true-sheet/navigation';
const Sheet = createTrueSheetNavigator({
screens: {
Main: MainScreen,
Details: createTrueSheetScreen({
screen: DetailsSheet,
options: { detents: ['auto', 1], cornerRadius: 16 },
}),
},
});
```
### Wrapping Existing Navigation [#wrapping-existing-navigation]
Wrap your root navigator to present sheets from anywhere:
```tsx
const Stack = createNativeStackNavigator();
const Sheet = createTrueSheetNavigator();
function RootStack() {
return (
);
}
function App() {
return (
);
}
```
### Nesting Navigators in Sheets [#nesting-navigators-in-sheets]
Native stacks are not supported inside a sheet screen. On Android, `react-native-screens` looks for the app's root view above its containers, and the sheet renders outside of it. Presenting a sheet screen whose component is a `createNativeStackNavigator` crashes with:
```
IllegalStateException: ScreenContainer is not attached under ReactRootView
```
Only the base screen (the first screen, or `initialRouteName`) can host a native stack, as shown in [Wrapping Existing Navigation](#wrapping-existing-navigation).
For multi-step flows inside a sheet, use one of these instead:
* **Stack sheets.** Make each step its own sheet screen and navigate between them. The presenting sheet stays visible underneath, and `navigation.pop()`, `popTo()`, and `popToTop()` walk back through the stack. This is the recommended approach.
```tsx
function SettingsSheet() {
const navigation = useTrueSheetNavigation();
return (
navigation.navigate('Profile')} />
navigation.pop()} />
);
}
```
With [Expo Router](#expo-router), keep each step as a sibling route under the `Sheet` layout instead of giving the flow its own `_layout.tsx` with a ``:
```
app/
├── _layout.tsx # Sheet layout
├── index.tsx # Base content
├── settings.tsx # Sheet screen
└── profile.tsx # Sheet screen, presented from settings
```
```tsx
// app/_layout.tsx
export default function SheetLayout() {
return (
);
}
// app/settings.tsx
export default function SettingsSheet() {
const router = useRouter();
return (
router.push('/profile')} />
router.back()} />
);
}
```
* **Local state.** Swap the sheet's content with plain React state when you don't need navigation history or deep links.
### Navigation & Resizing [#navigation--resizing]
```tsx
function DetailsSheet() {
const navigation = useTrueSheetNavigation();
return (
navigation.resize(1)} />
navigation.goBack()} />
);
}
```
### Screen Options [#screen-options]
All [TrueSheet props](../reference/configuration) are available as screen options, plus the following navigation-specific options:
| Option | Type | Description |
| ----------------------- | ---------- | --------------------------------------------------------------------------------------------------------------- |
| `detentIndex` | `number` | The detent index to present at. Defaults to `0`. |
| `reanimated` | `boolean` | Enable worklet-based position events for this screen. |
| `positionChangeHandler` | `function` | A callback that receives position change events. When `reanimated` is enabled, this must be a worklet function. |
### Reanimated Integration [#reanimated-integration]
Enable worklet-based position events for smooth UI thread animations:
```tsx
// In your navigator
{
'worklet';
// Access payload.position, payload.detentIndex, etc.
console.log(payload.position);
},
}}
/>
```
When `reanimated: true` is set, `react-native-reanimated` must be installed and `positionChangeHandler` must be a worklet function. The integration is lazy-loaded, so screens without `reanimated: true` don't require reanimated.
### Dynamic Header & Footer [#dynamic-header--footer]
Use [`navigation.setOptions()`](https://reactnavigation.org/docs/navigation-object/#setoptions) to set or update `header` and `footer` from within a sheet screen. This is useful when you need access to navigation state or sheet events.
```tsx
function DetailsSheet() {
const navigation = useTrueSheetNavigation();
const [detentIndex, setDetentIndex] = useState(0);
useEffect(() => {
const unsubscribe = navigation.addListener('sheetDetentChange', (e) => {
setDetentIndex(e.data.index);
});
return unsubscribe;
}, [navigation]);
useEffect(() => {
navigation.setOptions({
footer: (
{detentIndex > 0 && navigation.resize(0)} />}
navigation.goBack()} />
),
});
}, [navigation, detentIndex]);
return {/* ... */};
}
```
All [TrueSheet props](../reference/configuration) like `header`, `footer`, `grabber`, `dismissible`, etc. can be dynamically updated via `setOptions`.
### Scrollable Content [#scrollable-content]
[`scrollableRef`](../reference/configuration#scrollableref) and [`scrollableOptions`](../reference/configuration#scrollableoptions) are screen options like any other prop. Since the ref is created inside the screen component, set it with `setOptions`:
```tsx
function DetailsSheet() {
const navigation = useTrueSheetNavigation();
const scrollableRef = useRef(null);
useEffect(() => {
navigation.setOptions({ scrollableRef });
}, [navigation]);
return (
{/* ... */}
);
}
```
Bound the scroll view's height with `flex: 1` via the screen's `style` option.
```tsx
```
See the [Scrolling guide](scrolling) for more information.
### Screen Listeners [#screen-listeners]
Use `screenListeners` on the navigator or `listeners` on individual screens:
```tsx
console.log('Presented:', e.data.index),
sheetDidDismiss: () => console.log('Dismissed'),
}}
>
```
Or use `addListener` within a screen component:
```tsx
function DetailsSheet() {
const navigation = useTrueSheetNavigation();
useEffect(() => {
const unsubscribe = navigation.addListener('sheetDidPresent', (e) => {
console.log('Presented:', e.data.index);
});
return unsubscribe;
}, [navigation]);
return {/* ... */};
}
```
| Event | Description |
| --------------------- | --------------------------------------------- |
| `sheetWillPresent` | Sheet is about to present |
| `sheetDidPresent` | Sheet finished presenting |
| `sheetWillDismiss` | Sheet is about to dismiss |
| `sheetDidDismiss` | Sheet finished dismissing |
| `sheetDismissAttempt` | User tried to dismiss a non-dismissible sheet |
| `sheetDetentChange` | Detent changed |
| `sheetDragBegin` | User started dragging |
| `sheetDragChange` | User is dragging |
| `sheetDragEnd` | User stopped dragging |
| `sheetPositionChange` | Position changed |
See [Lifecycle Events](../reference/events) for more details.
## Expo Router [#expo-router]
TrueSheet ships a ready-to-use `Sheet` layout for Expo Router via the `/navigation/expo-router` entry point. It integrates with Expo Router's built-in navigation directly — no `@react-navigation/*` install needed. All [navigator features](#sheet-navigator) above (screen options, reanimated, dynamic header/footer, listeners) apply here too.
Requires Expo SDK 57+ (`expo-router` version `57.0.0` or higher). Install `standard-navigation`, declared as an optional peer dependency:
npm
pnpm
yarn
bun
```bash
npm install standard-navigation
```
```bash
pnpm add standard-navigation
```
```bash
yarn add standard-navigation
```
```bash
bun add standard-navigation
```
```
app/
├── _layout.tsx # TrueSheet navigator
├── index.tsx # Base content
└── details.tsx # Sheet screen
```
```tsx
// app/_layout.tsx
import { Sheet } from '@lodev09/react-native-true-sheet/navigation/expo-router';
export default function SheetLayout() {
return (
);
}
```
On **Expo SDK 58+**, Expo Router picks the base screen from the layout's `unstable_settings.anchor` and ignores the `initialRouteName` prop on `Sheet`:
```tsx
export const unstable_settings = {
anchor: 'index',
};
```
Inside sheet screens, import `useTrueSheetNavigation` from the same entry point:
```tsx
import { useTrueSheetNavigation } from '@lodev09/react-native-true-sheet/navigation/expo-router';
```
A sheet route must not have its own `_layout.tsx` with a ``. Keep multi-step flows as sibling sheet screens under the `Sheet` layout instead. See [Nesting Navigators in Sheets](#nesting-navigators-in-sheets).
See [Expo Router docs](https://docs.expo.dev/router/introduction/) for more information.
## Navigating from Sheets [#navigating-from-sheets]
Navigate directly from sheets - they remain visible when presenting modals on top.
```tsx
// Navigate directly - no need to dismiss first!
navigation.navigate('SomeScreen')
```
Requires a [patch to react-native-screens](https://github.com/lodev09/react-native-true-sheet/blob/main/.yarn/patches/react-native-screens-npm-4.25.2.patch). See [PR #3415](https://github.com/software-mansion/react-native-screens/pull/3415).
On **Expo SDK 56+**, this patch is silently dropped on EAS builds because `react-native-screens` ships precompiled. See [Patched react-native-screens Not Applied on EAS](../troubleshooting#patched-react-native-screens-not-applied-on-eas-precompiled-modules) to force it to build from source.
### Web Limitation [#web-limitation]
On native platforms, TrueSheet automatically detects `react-native-screens` and handles sheet visibility when navigating. However, this detection is not supported on web.
As a workaround, use `useFocusEffect` to manually present/dismiss the sheet when the screen gains or loses focus:
```tsx
import { useFocusEffect } from '@react-navigation/native';
function DetailsSheet() {
const sheet = useRef(null);
useFocusEffect(
useCallback(() => {
sheet.current?.present();
return () => {
sheet.current?.dismiss();
};
}, [])
);
return {/* ... */};
}
```
# Present On Mount (/next/guides/onmount)
Sometimes, you may want to present the sheet directly during mount. For example, you might want to present the sheet when a screen is opened through a deep link.
## How? [#how]
You can do this by setting [`initialDetentIndex`](../reference/configuration#initialdetentindex) prop. It accepts the [`detent`](../reference/types#sheetdetent) `index` that your sheet is configured with. See [detents](../reference/configuration#detents) prop for more information.
```tsx {5-6}
const App = () => {
return (
)
}
```
### Disabling Animation [#disabling-animation]
You may want to disable the present animation. To do this, simply set [`initialDetentAnimated`](../reference/configuration#initialdetentanimated) to `false`.
### Deep Linking on iOS [#deep-linking-on-ios]
iOS Limitation
When a deep link opens another screen over the sheet's screen on a cold start, the sheet may present on top of it or not at all. See [Deep Linking](deep-linking) for the workaround.
# Overlays on Sheets (/next/guides/overlays)
Sheets are presented natively, above the React Native view hierarchy — so a toast or dialog rendered in your app's tree (or through a JS portal) ends up **behind** the sheet.
`TrueSheetOverlay` renders its children in a native layer above every presented sheet.
## How? [#how]
Wrap the content with `TrueSheetOverlay` and show or hide it by conditionally rendering children — there is no `visible` prop.
```tsx {14-16}
import { TrueSheet, TrueSheetOverlay } from '@lodev09/react-native-true-sheet'
const App = () => {
const [toast, setToast] = useState(null)
return (
<>
setToast('Saved!')} />
{toast && setToast(null)} />}
>
)
}
```
The overlay can be mounted anywhere — on a screen, or inside a sheet's content.
## Touches [#touches]
Touches that miss the overlay's children pass through to whatever is beneath, so the sheet stays draggable and its buttons stay tappable around a toast.
To block interaction (e.g. a dialog with a backdrop), render a child that fills the overlay:
```tsx
{visible && (
)}
```
## Layout [#layout]
The overlay is a transparent container that fills the window. Position children absolutely, or lay them out with flex and padding through `style`:
```tsx
```
The overlay itself never renders — visual styles like `backgroundColor` have no effect on it. Style its children instead.
Since it fills the window, content may sit under the status bar or the system navigation bar. Use `useSafeAreaInsets` from `react-native-safe-area-context` to inset it.
## Limitations [#limitations]
* Native modals presented **after** the overlay — React Native's `Modal`, native-stack modal screens — render above it.
* The keyboard always renders above the overlay.
* `react-native-screens` containers — native stacks, bottom tabs, drawers — are not supported inside the overlay. On Android they crash with the same error described in [Nesting Navigators in Sheets](./navigation#nesting-navigators-in-sheets).
## Platform Support [#platform-support]
`TrueSheetOverlay` is supported on **iOS**, **Android**, and **Web**.
# Peeking Content (/next/guides/peeking)
Building a map-style sheet with a compact summary that expands into full content? The [`"peek"`](../reference/types#sheetdetent) detent collapses the sheet down to just the essentials — no manual height juggling needed.
## The `"peek"` Detent [#the-peek-detent]
Add `"peek"` to your [`detents`](../reference/configuration#detents) to get a collapsed size based on the measured [`header`](header) height, plus an [absolute footer](footer#floating-footer) if provided — a relative footer is laid out below the content, so it's pushed off-screen at peek and doesn't count.
```tsx {4-4}
const App = () => {
return (
}
footer={}
footerOptions={{ position: 'absolute' }}
>
)
}
```
The peek height automatically follows the measured header and footer sizes. When no `header`, absolute `footer`, or peeking content is provided, it falls back to a fixed height of `150`.
## Peeking Part of the Content [#peeking-part-of-the-content]
To include part of the content in the peek height, wrap it with the `TrueSheetPeek` component.
```tsx {8-10}
import { TrueSheet, TrueSheetPeek } from '@lodev09/react-native-true-sheet'
const App = () => {
return (
)
}
```
When collapsed, the sheet reveals everything from the top of the sheet through the **bottom edge** of `TrueSheetPeek` — content padding and anything rendered above it count toward the height, while content below it stays hidden until the sheet is expanded.
```
┌──────────────────────┐
│ header │ ─┐
│ ┌──────────────────┐ │ │
│ │ TrueSheetPeek │ │ ├─ visible when collapsed
│ └──────────────────┘ │ │
│ footer │ ─┘
├──────────────────────┤
│ rest of the content │ ── hidden until expanded
└──────────────────────┘
```
`TrueSheetPeek` can be placed anywhere within the content — the collapsed sheet simply reveals everything above it, including itself. A sheet can only have **one** peek component.
Since everything above it counts, an **empty** `TrueSheetPeek` works as a fold marker — drop it between your existing views to mark where the collapsed sheet ends, no wrapping needed.
```tsx {3}
```
## Styling [#styling]
`TrueSheetPeek` is a regular `View` — style it like any other.
```tsx
True SheetThe true native bottom sheet experience.
```
Note that only the content through its bottom edge counts — a `marginBottom` on the peek itself is *not* included. If you want breathing room below the peek content while collapsed, use `paddingBottom` instead.
## Safe Area [#safe-area]
By default ([`insetAdjustment="automatic"`](../reference/configuration#insetadjustment)), the bottom safe area inset is added to the peek height so the peek content clears the system navigation bar. Set `insetAdjustment="never"` if you're handling insets yourself.
## Resizing [#resizing]
`"peek"` works like any other detent — resize to it programmatically or listen for changes. See [Resizing Programmatically](resizing).
```tsx
sheet.current?.resize(0) // collapse back to peek
```
## Platform Support [#platform-support]
The `"peek"` detent and `TrueSheetPeek` are supported on **iOS 16+**, **Android**, and **Web**.
# Reanimated (/next/guides/reanimated)
`TrueSheet` has first-class support for [react-native-reanimated v4](https://docs.swmansion.com/react-native-reanimated/).
Requirements
* `react-native-reanimated`: ^4.0.0
* `react-native-worklets` (peer dependency of Reanimated v4)
## How? [#how]
### 1. Add the Provider [#1-add-the-provider]
Manages shared values for Reanimated integration.
```tsx
import { ReanimatedTrueSheetProvider } from '@lodev09/react-native-true-sheet/reanimated'
function App() {
return (
)
}
```
### 2. Use ReanimatedTrueSheet [#2-use-reanimatedtruesheet]
Animated sheet component that syncs position automatically with all props from [`TrueSheet`](../reference/configuration).
```tsx
import { type TrueSheet } from '@lodev09/react-native-true-sheet'
import { ReanimatedTrueSheet } from '@lodev09/react-native-true-sheet/reanimated'
function MyScreen() {
const sheetRef = useRef(null)
return (
Sheet Content
)
}
```
Note that the `onPositionChange` prop event now runs on the UI thread (worklet). If you override this prop, make sure to add the `'worklet'` directive to your handler.
### 3. Access Animated Values [#3-access-animated-values]
Use the `useReanimatedTrueSheet` hook to access the sheet's animated values.
```tsx
import { useReanimatedTrueSheet } from '@lodev09/react-native-true-sheet/reanimated'
import Animated, { useAnimatedStyle, interpolate, Extrapolation } from 'react-native-reanimated'
function MyComponent() {
const { animatedPosition, animatedIndex, animatedDetent } = useReanimatedTrueSheet()
// Example: Move component based on sheet position
const positionStyle = useAnimatedStyle(() => ({
transform: [{ translateY: -animatedPosition.value }]
}))
// Example: Fade in as sheet expands from index 0 to 1
const opacityStyle = useAnimatedStyle(() => ({
opacity: interpolate(animatedIndex.value, [0, 1], [0, 1], Extrapolation.CLAMP)
}))
return (
This moves and fades with the sheet
)
}
```
#### Available Values [#available-values]
| Value | Type | Description |
| ------------------ | --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `animatedPosition` | `SharedValue` | The current Y position of the sheet relative to the screen. |
| `animatedIndex` | `SharedValue` | The current detent index as a continuous float. Interpolates smoothly between detent indices during drag (e.g., `0.5` when halfway between index 0 and 1). |
| `animatedDetent` | `SharedValue` | The current detent value (0-1 fraction of screen height). Interpolates smoothly between detent values as the sheet is dragged. |
## Examples [#examples]
See the [example app](https://github.com/lodev09/react-native-true-sheet/tree/main/example) for complete implementations.
# Resizing Programmatically (/next/guides/resizing)
`TrueSheet` has a main prop called [`detents`](../reference/configuration#detents) which allows you to define the detents that the sheet can support. This is an array of [`SheetDetent`](../reference/types#sheetdetent) that supports values like `"auto"`, `"peek"`, or fractional numbers (0-1).
In some cases, you may want to resize the sheet programmatically for a better experience.
## How? [#how]
### Resize Programmatically [#resize-programmatically]
Define the sheet and use the [`resize`](../reference/methods#resize) method.
```tsx {2-2,6-6,11-13}
const App = () => {
const sheet = useRef(null)
const resize = async () => {
// Resize to 69%
await sheet.current?.resize(1)
console.log('Yay, we are now at 69% 💦')
}
return (
)
}
```
You can also do it globally using the related [global method](../reference/methods#global-methods).
```tsx
TrueSheet.resize('resizing-sheet', 1)
```
`detents` can only support up to 3 detents. ***collapsed***, ***half-expanded***, and ***expanded***.
The [`auto`](../reference/types#sheetdetent) detent works with scrolling content too — no `flex: 1` needed. The sheet sizes to the scroll view's content height and resizes as content grows or shrinks, while the viewport is automatically bounded to the sheet's visible height. See the [Scrolling guide](scrolling).
Use [`insetAdjustment="never"`](../reference/configuration#insetadjustment) to disable automatic bottom inset adjustment on both platforms.
If you want to disable user dragging and only allow programmatic resizing, set [`draggable={false}`](../reference/configuration#draggable).
### Listening to Detent Change [#listening-to-detent-change]
If you want to get the active detent information, you can listen to detent changes by providing the [`onDetentChange`](../reference/events#ondetentchange) event.
The event comes with the [`DetentInfoEventPayload`](../reference/types#detentinfoeventpayload) that provides the detent `index` and `position` (Y position on screen).
Use the `index` to reference the detent from your `detents` array. For example, if `detents={['auto', 0.69, 1]}` and `index` is `1`, the active detent is `0.69`.
```tsx {9-11,17-17}
const App = () => {
const sheet = useRef(null)
const resize = async () => {
// Resize to 69%
await sheet.current?.resize(1)
}
const handleDetentChange = (e: DetentChangeEvent) => {
const { index, position } = e.nativeEvent
console.log('Detent index:', index, 'position:', position) ✅
}
return (
)
}
```
The event will also trigger when the user drags the sheet into a detent.
# Scrolling Content (/next/guides/scrolling)
Scrolling content within the sheet works like a regular view — bound the scroll view's height with `flex: 1`, plug a `ScrollView` or `FlatList` in, and point the sheet at it with [`scrollableRef`](../reference/configuration#scrollableref).
## How? [#how]
Pass a ref of your scroll view (including `ScrollView` and `FlatList`) to the sheet via the [`scrollableRef`](../reference/configuration#scrollableref) prop. Keyboard handling and nested scrolling are then wired for you.
Like any scroll view in React Native, it needs a bounded height to scroll. Pass `flex: 1` via the sheet's [`style`](../reference/configuration#style) prop so the content fills the sheet's visible height per detent — resizing in realtime while dragging:
```tsx
const App = () => {
const sheet = useRef(null)
const scrollableRef = useRef(null)
return (
)
}
```
The scroll view doesn't have to be a direct child — wrapping it in a container `View` works too:
```tsx
{/* Header content */}
```
On Android, nested scrolling is managed internally — any `nestedScrollEnabled` prop on your `ScrollView` or `FlatList` is ignored. `RefreshControl` is also fully supported.
The [`auto`](../reference/types#sheetdetent) detent works with scrolling content too — no `flex: 1` needed. The sheet sizes to the scroll view's content height and resizes as content grows or shrinks, while the viewport is automatically bounded to the sheet's visible height.
## Bottom Safe-Area Inset [#bottom-safe-area-inset]
The sheet applies the bottom safe-area inset to your scroll content automatically while the content can scroll — the last item clears the home indicator (iOS) and navigation bar (Android):
* **iOS**: The scroll view's native [`contentInsetAdjustmentBehavior`](https://developer.apple.com/documentation/uikit/uiscrollview/contentinsetadjustmentbehavior) is set to `automatic` while it's plugged into the sheet — no need to set React Native's iOS-only prop yourself.
* **Android**: The inset is applied as content padding, mirroring iOS's `automatic` behavior — parity React Native's iOS-only prop can't provide.
* **Web**: The content is lifted above the safe area (`env(safe-area-inset-bottom)`).
To opt out and manage the padding yourself, set [`contentInsetAdjustment`](../reference/types#contentinsetadjustment) to `'never'` (or `'footer'` to keep the [footer inset](#footer-inset)) in [`scrollableOptions`](../reference/configuration#scrollableoptions):
```tsx
const insets = useSafeAreaInsets()
```
This only applies while the sheet's [`insetAdjustment`](../reference/configuration#insetadjustment) is `'automatic'`. If the sheet has a relative [`footer`](footer), the footer absorbs the bottom inset instead — the scroll view ends above it, so no padding is needed.
## Footer Inset [#footer-inset]
An absolute [`footer`](footer#floating-footer) floats over the content, covering the bottom of the scroll view. The sheet pads the scroll content by the footer's measured height automatically — the last item scrolls into view above the footer, and the footer counts toward the [`auto`](../reference/types#sheetdetent) detent so the content ends above it when the sheet is sized to its content. No `paddingBottom: FOOTER_HEIGHT` needed:
```tsx
}
footerOptions={{ position: 'absolute' }}
>
```
Only the part of the footer that overlaps the scroll view is applied — a short scroll view that ends above the footer gets no inset. The bottom safe-area inset the footer absorbs is not counted twice, and while the keyboard is open the inset stacks with the keyboard inset as the footer rises above it.
To let the content scroll under the footer instead, set [`contentInsetAdjustment`](../reference/types#contentinsetadjustment) to `'safe-area'` (keeps the safe-area inset) or `'never'`:
```tsx
}
footerOptions={{ position: 'absolute' }}
>
```
This only applies with `footerOptions.position` set to `'absolute'`. A relative footer sits below the content, so the scroll view already ends above it. Unlike the safe-area inset, it doesn't depend on the sheet's [`insetAdjustment`](../reference/configuration#insetadjustment) — the footer covers the scroll view either way. Remove any manual `paddingBottom` for the footer from your content, or it doubles up.
## Preventing Scroll-to-Expand [#preventing-scroll-to-expand]
By default, scrolling content to the top edge expands the sheet to the next detent. To disable this (e.g., for YouTube-style comments sheets where only the grabber should expand the sheet), set `scrollingExpandsSheet` to `false`:
```tsx
```
On iOS, this uses [`prefersScrollingExpandsWhenScrolledToEdge`](https://developer.apple.com/documentation/uikit/uisheetpresentationcontroller/3858107-prefersscrollingexpandswhenscrol). On Android, a custom `BottomSheetBehavior` selectively blocks scroll-driven expansion while preserving normal scrolling, fling deceleration, and grabber-based dragging.
## Scroll Edge Effect [#scroll-edge-effect]
On iOS 26+, you can apply a blur/gradient edge effect on the header and footer when content scrolls behind them. This uses [`UIScrollEdgeElementContainerInteraction`](https://developer.apple.com/documentation/uikit/uiscrolledgeelementcontainerinteraction) under the hood.
Set [`topScrollEdgeEffect`](../reference/types#scrollableoptions) and/or [`bottomScrollEdgeEffect`](../reference/types#scrollableoptions) in [`scrollableOptions`](../reference/configuration#scrollableoptions) to enable it:
```tsx
}
footer={}
>
```
Available values: `'automatic'`, `'hard'`, `'soft'`, or `'hidden'` (default). See [`ScrollEdgeEffect`](../reference/types#scrolledgeeffect).
This also controls the scroll view's own edge effects ([`UIScrollEdgeEffect`](https://developer.apple.com/documentation/uikit/uiscrolledgeeffect)). `topScrollEdgeEffect` applies to both the header interaction and the scroll view's top edge, and `bottomScrollEdgeEffect` applies to both the footer interaction and the scroll view's bottom edge.
This feature requires iOS 26 or later. On older versions, these options are ignored.
# Side Sheets (/next/guides/side-sheets)
Ever wanted your sheet to slide in from the side? Now it can! The [`placement`](../reference/configuration#placement) prop lets you dock the sheet to the leading or trailing edge — perfect for iPad and tablet layouts.
## How? [#how]
Set [`placement`](../reference/configuration#placement) to `"leading"` or `"trailing"` to position the sheet along that edge. Edges follow the layout direction, so `"leading"` is the left edge in LTR and the right edge in RTL.
```tsx {5,6}
export const App = () => {
return (
)
}
```
Combine with [`maxContentWidth`](../reference/configuration#maxcontentwidth) to control the sheet width when anchored.
### Adjusting the Edge Offset [#adjusting-the-edge-offset]
On Android and Web, you can control the margin from the screen edge using [`placementOffset`](../reference/configuration#placementoffset). The default is `16`.
```tsx {5}
export const App = () => {
return (
)
}
```
On iOS, the system controls edge margins. iOS 27+ uses `UISheetPresentationController`'s native `preferredPlacement`; earlier versions use `sourceView`. The sheet will always float slightly from the edge due to system-defined margins.
### Automatic vs Center [#automatic-vs-center]
The default `"automatic"` lets the system decide where the sheet goes. `"center"` asks for a centered sheet explicitly. The two only differ on iOS 27+, where they map to `preferredPlacement`. On Android, Web, and iOS 26 and below, both center the sheet.
`placement` and `maxContentWidth` are ignored on phones in portrait orientation — the sheet always spans the full width.
# Stacking Sheets (/next/guides/stacking)
TrueSheet automatically handles stacking when you present a sheet while another is already visible. The currently visible sheet will be hidden, and when the new sheet is dismissed, the previous sheet will be shown again.
## How? [#how]
Simply present a new sheet while another is visible. The sheets don't need to be in a parent-child relationship - they can be defined anywhere in your component tree.
```tsx
const presentSheet2 = async () => {
await sheet2.current?.present() // Sheet 2 will present, Sheet 1 will be hidden
}
return (
<>
>
)
```
When Sheet 2 is dismissed, Sheet 1 will automatically be shown again.
The previous sheet is only hidden if it's not fully expanded. When expanded, the new sheet simply presents on top.
## Dismissing Stacked Sheets [#dismissing-stacked-sheets]
When you have stacked sheets, calling `dismiss()` will dismiss the sheet and all sheets presented on top of it in a cascade animation.
```tsx
// If Sheet 1 → Sheet 2 → Sheet 3 (Sheet 1 presented Sheet 2, Sheet 2 presented Sheet 3)
await sheet1.current?.dismiss() // Dismisses Sheet 1, Sheet 2, and Sheet 3
await sheet2.current?.dismiss() // Dismisses Sheet 2 and Sheet 3, Sheet 1 stays
await sheet3.current?.dismiss() // Dismisses Sheet 3, Sheet 1 and Sheet 2 stay
```
### Dismiss Only Child Sheets [#dismiss-only-child-sheets]
If you want to dismiss only the sheets on top while keeping the current sheet presented, use the [`dismissStack()`](../reference/methods#dismissstack) method:
```tsx
// If Sheet 1 → Sheet 2 → Sheet 3
await sheet1.current?.dismissStack() // Dismisses Sheet 2 and Sheet 3, Sheet 1 stays
await sheet2.current?.dismissStack() // Dismisses Sheet 3, Sheet 1 and Sheet 2 stay
```
This is useful when you need to dismiss all child sheets and return focus to a parent sheet without closing it.
If no sheets are presented on top, `dismissStack()` does nothing.
### Dismiss All Sheets [#dismiss-all-sheets]
To dismiss all sheets in the current stack at once, use the static [`dismissAll()`](../reference/methods#dismissall) method:
```tsx
await TrueSheet.dismissAll()
```
Presentation Context
This only dismisses sheets in the current presentation context. Sheets presented behind a modal (e.g., React Navigation modal or React Native Modal) will not be affected.
## Focus Events [#focus-events]
When stacking sheets, you can listen to focus events to know when a sheet gains or loses focus:
* **`onDidBlur`** - Called when the sheet loses focus because another sheet is presented on top
* **`onDidFocus`** - Called when the sheet regains focus after a sheet on top is dismissed
This is useful for pausing/resuming updates, refreshing data, or managing interactions when the sheet is not the topmost.
```tsx {7-8}
const handleFocus = () => {
console.log('Sheet regained focus - refresh data')
}
const handleBlur = () => {
console.log('Sheet lost focus - pause updates')
}
return (
sheet2.current?.present()} title="Present Sheet 2" />
)
```
# Web Support (/next/guides/web)
Yes, `TrueSheet` supports web! It ships a vendored drawer implementation so the same API works across iOS, Android, and Web.
## Installation [#installation]
Install `@radix-ui/react-dialog` and `@radix-ui/react-presence` alongside `@lodev09/react-native-true-sheet`. They're declared as optional peer dependencies and provide the underlying dialog primitives for the web renderer.
npm
pnpm
yarn
bun
```bash
npm install @radix-ui/react-dialog @radix-ui/react-presence
```
```bash
pnpm add @radix-ui/react-dialog @radix-ui/react-presence
```
```bash
yarn add @radix-ui/react-dialog @radix-ui/react-presence
```
```bash
bun add @radix-ui/react-dialog @radix-ui/react-presence
```
## Setup [#setup]
Wrap your app with `TrueSheetProvider`:
```tsx {1,5-7}
import { TrueSheetProvider } from '@lodev09/react-native-true-sheet'
const App = () => {
return (
)
}
```
On native platforms, `TrueSheetProvider` is a simple pass-through component, so you can safely wrap your entire app without any performance impact.
## Controlling Sheets [#controlling-sheets]
On web, use the `useTrueSheet` hook or refs to control sheets. Static methods like `TrueSheet.present()` are not supported on web.
### Using the Hook [#using-the-hook]
Use the `useTrueSheet` hook to control sheets by name:
```tsx {1,4,7-8}
import { TrueSheet, useTrueSheet } from '@lodev09/react-native-true-sheet'
const MyComponent = () => {
const { present, dismiss, dismissAll } = useTrueSheet()
return (
<>
present('my-sheet')} title="Open" />
dismiss('my-sheet')} title="Close" />
Hello from the sheet!
>
)
}
```
### Using Refs [#using-refs]
You can also use refs to control sheets directly as you would on native.
## Detached Mode [#detached-mode]
Use the `detached` prop to render the sheet as a floating card, not attached to the bottom edge. Use `detachedOffset` to control the spacing from the bottom:
```tsx
{/* Content */}
```