Migrating to v4
Migration guide from v3 to v4
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.
Breaking Changes
1. React Native 0.82+ 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
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:
// ❌ v3
<TrueSheet scrollable detents={[0.5, 1]}>
<ScrollView>{/* ... */}</ScrollView>
</TrueSheet>
// ✅ v4 — plug the scroll view via scrollableRef, bound the content with flex: 1
<TrueSheet scrollableRef={scrollableRef} style={{ flex: 1 }} detents={[0.5, 1]}>
<ScrollView ref={scrollableRef}>{/* ... */}</ScrollView>
</TrueSheet>
// ✅ v4 — 'auto' detent sizes to the scroll content, no flex needed
<TrueSheet scrollableRef={scrollableRef} detents={['auto']}>
<ScrollView ref={scrollableRef}>{/* ... */}</ScrollView>
</TrueSheet>See the Scrolling guide for more information.
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:
// ❌ v3 — content filled the sheet implicitly
<TrueSheet detents={[0.5]}>
<View style={{ flex: 1, justifyContent: 'center' }}>
<Text>Centered</Text>
</View>
</TrueSheet>
// ✅ v4 — fill the sheet explicitly
<TrueSheet style={{ flex: 1 }} detents={[0.5]}>
<View style={{ flex: 1, justifyContent: 'center' }}>
<Text>Centered</Text>
</View>
</TrueSheet>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:
// ❌ v3 — footer floats over the content
<TrueSheet footer={<MyFooter />}>
{/* content */}
</TrueSheet>
// ✅ v4 — same floating behavior, now opt-in
<TrueSheet footer={<MyFooter />} footerOptions={{ position: 'absolute' }}>
{/* content */}
</TrueSheet>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 for more details.
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:
// ❌ v3 — manual safe-area padding
const MyFooter = () => {
const insets = useSafeAreaInsets();
return (
<View style={{ paddingBottom: insets.bottom, backgroundColor: '#333' }}>
<FooterContent />
</View>
);
};
// ✅ v4 — the footer absorbs the inset natively
<TrueSheet footer={<FooterContent />} footerStyle={{ backgroundColor: '#333' }}>
{/* content */}
</TrueSheet>See the Footer guide for more details.
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" 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:
// ✅ v4 — the safe-area and footer insets are applied natively
<TrueSheet scrollableRef={scrollableRef} footer={<Footer />} footerOptions={{ position: 'absolute' }}>
<ScrollView ref={scrollableRef}>{/* ... */}</ScrollView>
</TrueSheet>
// ✅ v4 — opt out and pad the content yourself
const insets = useSafeAreaInsets();
<TrueSheet
scrollableRef={scrollableRef}
scrollableOptions={{ contentInsetAdjustment: 'never' }}
>
<ScrollView ref={scrollableRef} contentContainerStyle={{ paddingBottom: insets.bottom }}>
{/* ... */}
</ScrollView>
</TrueSheet>'safe-area' keeps only the safe-area inset (content scrolls under the footer) and 'footer' keeps only the footer inset. See Scrolling Content.
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+
The navigator is now built on 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 install @react-navigation/native@^7.3.0 standard-navigationYour 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
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 install standard-navigationMigration:
// ❌ 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<string, object | undefined>;
const { Navigator } = createTrueSheetNavigator();
const Sheet = withLayoutContext<
TrueSheetNavigationOptions,
typeof Navigator,
TrueSheetNavigationState<ParamListBase>,
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:
// ❌ 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 for more details.
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:
// ❌ v3 — color and blur blend
<TrueSheet backgroundColor="rgba(0, 122, 255, 0.25)" backgroundBlur="system-material" />
// ✅ v4 — tint the blur through the background wrapper
<TrueSheet
backgroundBlur="system-material"
backgroundStyle={{ backgroundColor: 'rgba(0, 122, 255, 0.25)' }}
/>For tinted Liquid Glass, use backgroundStyle without backgroundColor or backgroundBlur:
<TrueSheet backgroundStyle={{ backgroundColor: 'rgba(0, 122, 255, 0.25)' }} />For an exact opaque color, including over full-screen modals, use backgroundStyle.backgroundColor:
<TrueSheet backgroundStyle={{ backgroundColor: '#18202b' }} />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 for details.
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.
// ❌ v3
<TrueSheet anchor="left" anchorOffset={24} maxContentWidth={400} />
// ✅ v4
<TrueSheet placement="leading" placementOffset={24} maxContentWidth={400} />See the Side Sheets guide for more details.
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:
// ❌ v3
<TrueSheet backgroundBlur="dark" blurOptions={{ intensity: 40, interaction: false }} />
// ✅ v4 — with expo-blur installed
import { BlurView } from 'expo-blur';
import { StyleSheet } from 'react-native';
<TrueSheet
backgroundColor="transparent"
background={<BlurView intensity={40} tint="dark" style={StyleSheet.absoluteFill} />}
/>The blur library controls platform support. See Custom blur.
New Features in v4
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.
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.
<TrueSheet header={<MyHeader />} headerOptions={{ position: 'absolute' }}>
{/* content */}
</TrueSheet>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
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.
5. Static API with createTrueSheetScreen
The navigator now supports React Navigation's static API:
const Sheet = createTrueSheetNavigator({
screens: {
Main: MainScreen,
Details: createTrueSheetScreen({
screen: DetailsSheet,
options: { detents: ['auto', 1] },
}),
},
});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
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:
<TrueSheet footer={<MyFooter />} footerOptions={{ position: 'absolute', avoidKeyboard: false }}>
{/* content */}
</TrueSheet>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.
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.
// ❌ v3 — platform-specific workaround
const Overlay = Platform.select({
ios: FullWindowOverlay,
default: Modal,
})
<Overlay visible={visible} transparent>
<Dialog onClose={() => setVisible(false)} />
</Overlay>
// ✅ v4
import { TrueSheetOverlay } from '@lodev09/react-native-true-sheet'
<TrueSheetOverlay>
{visible && <Dialog onClose={() => setVisible(false)} />}
</TrueSheetOverlay>See the Overlays guide for touch handling, layout, and limitations.
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:
// Exact color, including over full-screen modals
<TrueSheet backgroundStyle={{ backgroundColor: '#18202b' }} />
// Tint the blur
<TrueSheet
backgroundBlur="system-material"
backgroundStyle={{ backgroundColor: 'rgba(0, 122, 255, 0.25)' }}
/>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 and the Backgrounds guide.
10. Per-Detent Backgrounds
The new detentBackgrounds prop replaces JavaScript background changes in onDetentChange:
<TrueSheet
detents={['peek', 'auto', 1]}
detentBackgrounds={[null, { blur: 'system-material' }, '#18202b']}
/>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.
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:
<TrueSheet detents={[0.75]} dismissThreshold="short">
{/* content */}
</TrueSheet>Works on Android and Web. iOS always uses the system behavior. See dismissThreshold.
12. Dim Color and Opacity
The new dimColor and dimOpacity props change the color and strength of the dim behind the sheet:
<TrueSheet detents={['auto', 1]} dimColor="#1b0a3c" dimOpacity={0.3}>
{/* content */}
</TrueSheet>On iOS, they replace the system dim with a custom dim layer. See the Dimming guide.
Step-by-Step Migration
- Update React Native to 0.82 or newer.
- Update the package:
npm install @lodev09/react-native-true-sheet@^4.0.0 - Clean and reinstall iOS dependencies:
cd ios rm -rf Pods Podfile.lock build pod install cd .. - Clean Android build:
cd android ./gradlew clean cd .. - Update your code:
- Replace the
scrollableprop withscrollableRef, 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.backgroundColorfor glass/blur tints and exact opaque colors - Remove
blurOptions; use a third-party blur inbackgroundfor custom intensity - Rename
anchortoplacementandanchorOffsettoplacementOffset, with"left"/"right"becoming"leading"/"trailing" - Replace
FullWindowOverlay/Modaloverlay workarounds withTrueSheetOverlay - If using the Sheet Navigator, install
standard-navigationand update@react-navigation/nativeto 7.3+ - If using Expo Router, install
standard-navigation, replace thewithLayoutContextwrapper with theSheetlayout from/navigation/expo-router, and updateuseTrueSheetNavigationimports
- Replace the
- Test your app:
npx react-native run-ios npx react-native run-android
Need Help?
If you encounter any issues during migration:
- Check the Troubleshooting guide
- Open an issue on GitHub
- Review the example app for reference implementations