# Installation (/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 |
On React Native 0.81 (Expo SDK 54), install `@lodev09/react-native-true-sheet@^3` and follow the [v3 docs](/v3/intro).
The core sheet works on Expo SDK 55+. The [Expo Router `Sheet` layout](guides/navigation#expo-router) requires Expo SDK 57+.
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#component-methods) or [lifecycle events](reference/events)
* β¨οΈ **Keyboard Handling** - Built-in [keyboard handling](guides/keyboard) with automatic adjustment
* π **Side Sheets** - Native [side sheet](guides/side-sheets) support for iPad and Android tablets
* π¨ **Backgrounds** - Colors, blur, glass tints, and [per-detent backgrounds](guides/backgrounds) on all platforms
* π **Overlays** - Render toasts and dialogs [above your sheets](guides/overlays) with `TrueSheetOverlay`
* πͺ **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 & Expo Router** - Built-in [sheet navigator](guides/navigation) and [Expo Router `Sheet` layout](guides/navigation#expo-router)
* π± **Foldables** - [Rest the sheet on the hinge](guides/foldables) of a half-folded iPhone (iOS 27.1+)
* π **Web Support** - Full [web support](guides/web) out of the box
# Migrating to v3 (/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-replaced-by-scrollableref).
#### `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/v3/.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 (/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).
### 12. `onWillDismiss` Fires Only on a Committed Dismissal (iOS) [#12-onwilldismiss-fires-only-on-a-committed-dismissal-ios]
On iOS, [`onWillDismiss`](reference/events#onwilldismiss) used to fire as soon as a drag-to-dismiss began, even if the user then released the sheet back to a detent. It now fires only once the dismissal is committed. Move any work that should run when a drag merely starts to [`onDragBegin`](reference/events#ondragbegin).
### 13. `present()` and `dismiss()` Resolve After the Animation (Web) [#13-present-and-dismiss-resolve-after-the-animation-web]
On Web, `present()` and `dismiss()` now resolve when the animation ends instead of immediately. Calling them on an already presented or dismissed sheet is now a no-op with a warning, like native, so `present()` no longer resizes an open sheet. Use [`resize()`](reference/methods#resize) to change the detent of an open sheet.
## 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).
### 13. Foldables [#13-foldables]
On iOS 27.1+ foldables like the iPhone Duo, the new `foldOptions` prop rests the sheet on the hinge when the device is half-folded (tabletop), so it fills the panel below it:
```tsx
{/* content */}
```
`hingeBehavior: 'avoid'` keeps the sheet below the hinge, and `'snap'` also moves it there when the device folds. Nothing changes on other devices. See the [Foldables guide](guides/foldables).
### 14. Autofocus Inputs [#14-autofocus-inputs]
`autoFocus` on a `TextInput` inside the sheet now raises the keyboard together with the sheet. The v3 workaround of focusing in `onDidPresent` is no longer needed:
```tsx
// β v3 β focus after the sheet lands
inputRef.current?.focus()}>
// β
v4
```
With `lazy={false}`, content stays mounted after dismiss, so `autoFocus` focuses on the first present only. Focus in `onWillPresent` instead to raise the keyboard with the sheet on every present. See [Keyboard Handling](guides/keyboard#autofocus).
### 15. Confirm Dismissals with `onDismissAttempt` [#15-confirm-dismissals-with-ondismissattempt]
The new [`onDismissAttempt`](reference/events#ondismissattempt) event fires when the user tries to dismiss a sheet with `dismissible` set to `false` β by dragging it down, pressing back (Android) or escape (Web), or using an accessibility dismiss action. Confirm unsaved changes, then call `dismiss()`:
```tsx
confirmDiscard(() => sheet.current?.dismiss())}
>
{/* form */}
```
### 16. Mount Content Before Presenting with `lazy` [#16-mount-content-before-presenting-with-lazy]
Content mounts on first presentation by default. Set the new [`lazy`](reference/configuration#lazy) prop to `false` to mount it ahead of time, then call `present()` once it is ready so `auto` detents measure the settled content.
### 17. Scrollable Keyboard Offset [#17-scrollable-keyboard-offset]
The new `scrollableOptions.keyboardOffset` adjusts the bottom inset the scrollable gets while the keyboard is open. Pass a negative value to cancel out your own bottom padding. See [`ScrollableOptions`](reference/types#scrollableoptions).
### 18. Deep Linking [#18-deep-linking]
The new [Deep Linking guide](guides/deep-linking) shows how to hold `initialDetentIndex` until the screen is focused, so a cold-start deep link to a modal or sheet-navigator sheet doesn't get the sheet stacked on top.
### 19. Expo SDK 58 and React Native 0.88 [#19-expo-sdk-58-and-react-native-088]
v4 supports Expo SDK 58 and React Native 0.88, including the Expo Router `Sheet` layout on Router 58. See the [Expo Router guide](guides/navigation#expo-router) for the SDK 58 `anchor` setting.
## 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`
* On iOS, move work that should run when a drag starts from `onWillDismiss` to `onDragBegin`
* On Web, use `resize()` instead of calling `present()` on an open sheet
* 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 (/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/v3/.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.
* **`FlashList` renders blank with `auto`** β it wraps its scroll view in a `flex: 1` view, which collapses to zero height when the content isn't filled. Pass `flex: 1` via the sheet's [`style`](reference/configuration#style) prop; the `auto` detent still sizes the sheet to the list. See [Scrolling Content](guides/scrolling).
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 (/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.
# Backgrounds (/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 (/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
```
# Detents (/guides/detents)
Detents are the heights the sheet snaps to. The user drags the sheet between them, and you can resize it from code. Pass up to **3** detents to [`detents`](../reference/configuration#detents), sorted from smallest to largest.
## Detents [#detents]
Each detent is a [`SheetDetent`](../reference/types#sheetdetent):
| Value | Height | π | π€ | π |
| -------- | ------------------------------------------------------------------------------------ | --------- | -- | -- |
| `'auto'` | Fits the content | ***16+*** | β
| β
|
| `'peek'` | Collapses to the header, an absolute footer, and the content above a `TrueSheetPeek` | ***16+*** | β
| β
|
| `number` | A fraction (0-1) of the screen height | β
| β
| β
|
```tsx {4}
const App = () => {
return (
)
}
```
The default is `[0.5, 1]`.
### Fixed Heights [#fixed-heights]
A fraction sets the sheet's height relative to the screen: `0.5` is half the screen and `1` is the full height. The content wraps its children like a regular view. To fill the sheet's visible height at each detent, pass `flex: 1` via [`style`](../reference/configuration#style):
```tsx {4}
const App = () => {
return (
Centered
)
}
```
The content resizes in the same frame as the sheet and follows its edge while the user drags, so flex layouts, like a bottom-pinned button, stay in place.
### Sizing to the Content [#sizing-to-the-content]
The `'auto'` detent sizes the sheet to the content's height. A relative [`header`](header) and [`footer`](footer) count toward it too. The sheet resizes when the content changes.
```tsx {4}
const App = () => {
return (
Sized to fit
)
}
```
Combine it with a fraction to let the user expand the sheet, e.g. `['auto', 1]`.
### Auto with Scrollables [#auto-with-scrollables]
`'auto'` also works with a `ScrollView` or `FlatList` passed to [`scrollableRef`](../reference/configuration#scrollableref). The sheet sizes to the scroll content and grows with it. When the content is taller than the screen, the sheet stops at its maximum height and the list scrolls. No `flex: 1` is needed:
```tsx {2,5,7}
const App = () => {
const scrollableRef = useRef(null)
return (
}
/>
)
}
```
Lists that wrap their scroll view in a `flex: 1` view, like `FlashList`, still need `style={{ flex: 1 }}` on the sheet. See the [Scrolling guide](scrolling).
### Peek [#peek]
`'peek'` collapses the sheet to its header, an absolute footer, and the content above a `TrueSheetPeek` marker. Use it for map-style sheets that expand from a compact summary. See [Peeking Content](peeking).
## Resizing Programmatically [#resizing-programmatically]
Call [`resize`](../reference/methods#resize) with a detent **index**, not a value.
```tsx {2,6,11}
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)
```
[`present`](../reference/methods#present) also takes an index, and [`initialDetentIndex`](../reference/configuration#initialdetentindex) presents the sheet at an index when it mounts. See [Present on Mount](onmount).
To allow only programmatic resizing, set [`draggable={false}`](../reference/configuration#draggable).
## Listening to Detent Changes [#listening-to-detent-changes]
[`onDetentChange`](../reference/events#ondetentchange) fires when the sheet settles at a detent, from a drag or a `resize` call, and when [changing `detents`](#changing-detents) moves the sheet to another detent or index. It comes with [`DetentInfoEventPayload`](../reference/types#detentinfoeventpayload): the detent `index` and its `position` (Y position on screen).
```tsx {2-5,10}
const App = () => {
const handleDetentChange = (e: DetentChangeEvent) => {
const { index, position } = e.nativeEvent
console.log('Detent index:', index, 'position:', position) β
}
return (
)
}
```
Use the `index` to look up the detent in your `detents` array. With `detents={['auto', 0.69, 1]}`, an `index` of `1` means the sheet is at `0.69`.
To track the sheet's position while it moves, use [`onPositionChange`](../reference/events#onpositionchange) or the [Reanimated integration](reanimated).
## Changing Detents [#changing-detents]
`detents` can change while the sheet is presented, for example to add a `'peek'` detent while a page can collapse to its header. The sheet stays at the detent it's at, and [`onDetentChange`](../reference/events#ondetentchange) reports its new index:
```tsx
// At 0.45 (index 0). Adding 'peek' keeps it at 0.45, now index 1
```
If the detent the sheet is at is removed, the sheet moves to the detent now at the same index.
Props that take a detent index, like [`detentBackgrounds`](../reference/configuration#detentbackgrounds) and [`dimmedDetentIndex`](../reference/configuration#dimmeddetentindex), don't follow along. Change them with `detents`.
A [`resize`](../reference/methods#resize) or [`present`](../reference/methods#present) called right after changing `detents` can reach the native sheet before the new detents do, and run against the old ones. On Android this includes calls from an effect. Defer the call so the change lands first:
```tsx
const showPlace = () => {
setHasPeek(true)
setTimeout(() => sheet.current?.resize(2))
}
```
## Detent Options [#detent-options]
These props take a detent index or change with the detent:
| Prop | Use |
| --------------------------------------------------------------------- | ------------------------------------------------------------------------------ |
| [`initialDetentIndex`](../reference/configuration#initialdetentindex) | Present on mount at this detent |
| [`dimmedDetentIndex`](../reference/configuration#dimmeddetentindex) | Start dimming the background at this detent. See [Dimming](dimming) |
| [`detentBackgrounds`](../reference/configuration#detentbackgrounds) | Change the background per detent. See [Backgrounds](backgrounds) |
| [`foldOptions`](../reference/configuration#foldoptions) | Rest a detent on the hinge of a half-folded iPhone. See [Foldables](foldables) |
| [`maxContentHeight`](../reference/configuration#maxcontentheight) | Cap the sheet's height, whatever the detent |
# Dimming (/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.
# Dismissing (/guides/dismissing)
The user can dismiss a sheet by dragging it down, tapping the dim, pressing the back button (Android), or pressing escape (Web). This guide shows how to block these gestures, confirm before the sheet closes, and change how far the user must drag.
## Preventing Dismissal [#preventing-dismissal]
Set [`dismissible`](../reference/configuration#dismissible) to `false` to keep the sheet presented. The user can still drag it between detents, but it springs back when they drag it down. Close it from code with [`dismiss`](../reference/methods#dismiss).
```tsx {7}
export const App = () => {
const sheet = useRef(null)
return (
)
}
```
[`draggable={false}`](../reference/configuration#draggable) stops the user from dragging the sheet, but a tap on the dim still dismisses it. Use `dismissible={false}` to block both.
## Confirming Dismissal [#confirming-dismissal]
When `dismissible` is `false`, a blocked attempt fires [`onDismissAttempt`](../reference/events#ondismissattempt). Use it to confirm before discarding unsaved changes, then call `dismiss()` to proceed.
```tsx {8-15}
export const App = () => {
const sheet = useRef(null)
const [isDirty, setIsDirty] = useState(false)
return (
{
Alert.alert('Discard changes?', undefined, [
{ text: 'Keep editing', style: 'cancel' },
{ text: 'Discard', style: 'destructive', onPress: () => sheet.current?.dismiss() },
])
}}
onDidDismiss={() => setIsDirty(false)}
>
setIsDirty(true)} />
)
}
```
These count as an attempt:
* Dragging the sheet down
* Pressing the back button (Android) or escape (Web)
* The accessibility dismiss action, e.g. the VoiceOver escape gesture
A tap on the dim does not count, matching iOS.
Native can't wait for JavaScript to decide, so you can't block a dismissal from a callback. Toggle `dismissible` from state before the user tries to dismiss.
On Android, `onDismissAttempt` consumes the back press while `dismissible` is `false`. Return `false` from [`onBackPress`](../reference/events#onbackpress) to let it propagate, e.g. to go back to the previous screen.
### With Navigation [#with-navigation]
In the [sheet navigator](navigation), set `dismissible` as a screen option and listen for `sheetDismissAttempt`. Call `navigation.goBack()` to close the sheet.
```tsx
function EditSheet() {
const navigation = useTrueSheetNavigation()
const [isDirty, setIsDirty] = useState(false)
useEffect(() => {
navigation.setOptions({ dismissible: !isDirty })
}, [navigation, isDirty])
useEffect(() => {
return navigation.addListener('sheetDismissAttempt', () => {
Alert.alert('Discard changes?', undefined, [
{ text: 'Keep editing', style: 'cancel' },
{ text: 'Discard', style: 'destructive', onPress: () => navigation.goBack() },
])
})
}, [navigation])
return setIsDirty(true)} />
}
```
## Dismiss Threshold [#dismiss-threshold]
On Android and Web, [`dismissThreshold`](../reference/configuration#dismissthreshold) sets how far the user must drag the sheet down from its lowest detent to dismiss it.
```tsx {4}
export const App = () => {
return (
)
}
```
| Value | Behavior |
| --------- | ------------------------------------------------------------------------------------------------ |
| `'half'` | Dismisses past half of the lowest detent, or on a quick flick. Matches iOS. This is the default. |
| `'short'` | Dismisses on a short drag or any downward fling, like Compose Material3's `ModalBottomSheet`. |
On iOS, the system controls the threshold.
## Dismissing Programmatically [#dismissing-programmatically]
| Method | Behavior |
| --------------------------------------------------------- | --------------------------------------------------------------- |
| [`dismiss`](../reference/methods#dismiss) | Dismisses the sheet and all sheets presented on top of it. |
| [`dismissStack`](../reference/methods#dismissstack) | Dismisses only the sheets on top, keeping this sheet presented. |
| [`TrueSheet.dismissAll`](../reference/methods#dismissall) | Dismisses every presented sheet. |
All of them ignore `dismissible`. Listen for [`onWillDismiss`](../reference/events#onwilldismiss) and [`onDidDismiss`](../reference/events#ondiddismiss) to react to any dismissal.
# 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.
# Foldables (/guides/foldables)
Foldables like the iPhone Duo can stand half-open on a table, with the screen split by the hinge. A sheet that spans the hinge is hard to use there. With [`foldOptions`](../reference/configuration#foldoptions), the sheet can rest right on the hinge and fill the panel below it, like a media player on a laptop-style screen.
Requires iOS 27.1+ on a foldable. On other devices, platforms, and iOS versions, `foldOptions` has no effect.
## Postures [#postures]
A foldable can be in one of these postures. `foldOptions` only acts in **tabletop**.
| Posture | Fold | Sheet behavior |
| ------------ | --------------------- | -------------------------------------------------------------------- |
| Closed | none (cover screen) | Like a phone |
| Flat | inactive | Like a tablet, the sheet spans the whole screen |
| Book | vertical, half-open | iOS docks the sheet to one panel β see [Book Posture](#book-posture) |
| **Tabletop** | horizontal, half-open | The hinge splits the screen into a top and a bottom panel |
## Resting on the Hinge [#resting-on-the-hinge]
Set [`hingeDetentIndex`](../reference/types#foldoptions) to the detent that should rest on the hinge. In tabletop, that detent resolves to the height of the bottom panel. In any other posture, it keeps its own value, so it doubles as the fallback.
```tsx {5}
export const App = () => {
return (
)
}
```
Here the sheet opens at `0.5` on a flat screen. Half-fold the device and detent `0` moves onto the hinge. The other detents keep their values, so the sheet can still expand past the hinge to `1`. They also keep their order: a lower detent never rises above the hinge detent, and a higher one never drops below it.
## Keeping the Sheet Off the Hinge [#keeping-the-sheet-off-the-hinge]
Set [`hingeBehavior`](../reference/types#foldoptions) to `'avoid'` to cap every detent at the hinge. The sheet never crosses it, even at its tallest detent.
```tsx {5}
export const App = () => {
return (
)
}
```
Detents that reach above the hinge collapse onto it, so they all stop at the same height. `'avoid'` works best with one detent above the hinge.
## Snapping to the Hinge [#snapping-to-the-hinge]
`'avoid'` keeps the sheet where it is when the device folds. A sheet resting low stays low. Set `hingeBehavior` to `'snap'` to also move the sheet to `hingeDetentIndex` when the device enters tabletop, and back to its previous detent when it leaves. If the user moves the sheet off the hinge while folded, it stays where they put it.
```tsx {5-8}
export const NowPlaying = () => {
return (
)
}
```
On a flat screen this is a mini player at `peek`. Half-fold the device and the full controls fill the bottom panel. Unfold it and the sheet goes back to the mini player.
| `hingeBehavior` | Crosses the hinge | Moves on fold |
| --------------- | ----------------- | ----------------------------------- |
| `'none'` | Yes | No |
| `'avoid'` | No | No |
| `'snap'` | No | Yes, to `hingeDetentIndex` and back |
The usual [`onDetentChange`](../reference/events#ondetentchange) and [`onPositionChange`](../reference/events#onpositionchange) events fire when the sheet moves for the hinge.
## Book Posture [#book-posture]
With a vertical fold, iOS already keeps the sheet on one panel, so `foldOptions` has nothing to do there. Use [`placement`](../reference/configuration#placement) to pick the panel. `'automatic'` and `'center'` dock to the leading panel.
```tsx {4}
export const App = () => {
return (
)
}
```
## Navigation [#navigation]
`foldOptions` is a screen option in the [Sheet Navigator](navigation) and the Expo Router `Sheet` layout, like any other prop.
```tsx {5-8}
```
## Testing on the Simulator [#testing-on-the-simulator]
Use the **iPhone Duo** simulator on iOS 27.1+.
1. Unfold the device, then rotate it so the UI is in portrait. The fold is now horizontal.
2. Present your sheet.
3. Half-fold the device to enter tabletop. Unfold it to leave.
The example app's **TrueSheet View** sheet uses `foldOptions` with `hingeBehavior: 'snap'`.
# Footer (/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 (/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') // β
```
# 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.
## 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 (/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 (/guides/keyboard)
TrueSheet handles keyboard visibility on iOS, Android, and mobile web. 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 each platform's implementation:
* **iOS**: Uses `UISheetPresentationController`'s built-in keyboard avoidance
* **Android**: Tracks keyboard height via `WindowInsetsAnimationCompat` and reconfigures sheet detents in real-time
* **Web**: Fits the sheet to the visible area above the soft keyboard (the [visual viewport](https://developer.mozilla.org/en-US/docs/Web/API/Visual_Viewport_API)). Detents keep their size and cap at the visible height, and a focused input is scrolled back into view
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 [#autofocus]
Use `autoFocus` on a `TextInput` inside the sheet to raise the keyboard together with the sheet:
```tsx
```
`autoFocus` only applies when the input mounts. With [`lazy`](../reference/configuration#lazy) set to `false`, the content stays mounted after dismiss, so the input focuses on the first present only.
To focus on every present, focus the input programmatically. Focus it in `onWillPresent` to raise the keyboard together with the sheet, or in `onDidPresent` to raise it after the sheet lands:
```tsx {4,7}
const inputRef = useRef(null)
const handleWillPresent = () => {
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.
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 (/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 (
);
}
```
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.26.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
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 (/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 (/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 Sheet
The 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 [Detents](detents#resizing-programmatically).
```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.1.0
* `react-native-worklets`: ^0.5.0 (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.
# Scrolling Content (/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. See [Auto with Scrollables](detents#auto-with-scrollables).
Lists that wrap their scroll view in a `flex: 1` view, like [`FlashList`](https://shopify.github.io/flash-list/), still need `flex: 1` on the sheet's `style` with `auto`. Without it, the wrapper collapses to zero height and the list renders blank. The sheet still sizes to the list's content:
```tsx
```
## 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 (/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 (/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/detents) 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` | | β
| β
|
## `foldOptions` [#foldoptions]
How the sheet behaves on a foldable while it's half-open with a horizontal fold (tabletop). See [this guide](../guides/foldables) for example.
| Type | Default | π | π€ | π |
| ---------------------------------- | ------- | -- | -- | -- |
| [`FoldOptions`](types#foldoptions) | | β
| | |
```tsx
```
Requires iOS 27.1+ on a foldable.
In book posture (vertical fold), iOS already docks every sheet to one panel. Use [`placement`](#placement) to pick the side.
## `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. Lists that expose an imperative handle (e.g. `LegendList`, `FlashList`) also work, through their `getScrollableNode()` or `getNativeScrollRef()`. 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 (/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: by dragging, by programmatically [resizing](methods#resize) it, or when a [`detents`](configuration#detents) change moves the sheet to another detent or index. See [Changing Detents](../guides/detents#changing-detents).
## `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', 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 (/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` |
## `FoldOptions` [#foldoptions]
Options for foldables. They apply while the device is half-open with a horizontal fold (tabletop). Requires iOS 27.1+.
```tsx
{/* ... */}
```
| Property | Type | Description | Default |
| ------------------ | ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- |
| `hingeDetentIndex` | `number` | The detent that rests on the hinge in tabletop, so the sheet fills the panel below it. In any other posture the detent keeps its own value. Other detents keep their values, so the sheet can still expand past the hinge unless `hingeBehavior` keeps it below. Lower detents stay below it and higher ones above it, so the detents keep their order. | |
| `hingeBehavior` | `'none' \| 'avoid' \| 'snap'` | How the sheet treats the hinge in tabletop. `'none'` lets detents cross it. `'avoid'` caps every detent at the hinge, so the sheet never crosses it. `'snap'` is like `'avoid'`, and also moves the sheet to `hingeDetentIndex` when the device enters tabletop, then back to its previous detent when it leaves, unless the sheet was moved off the hinge in between. Detents that reach above the hinge collapse onto it, so `'avoid'` works best with one detent above the hinge. | `'none'` |
## `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. |