🚧 True Sheet 4.0 beta rewrites the layout engine. npx expo install @lodev09/react-native-true-sheet@beta Migration guide

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>

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.

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-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

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-navigation

Migration:

// ❌ 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. backgroundColor No Longer Disables Liquid Glass

On iOS 26+, backgroundColor now paints over the Liquid Glass background instead of replacing it. Opaque colors look the same as before. A translucent color now tints the glass instead of rendering a see-through flat color. To get the v3 flat background, set the new glass prop to false.

// ❌ v3 — backgroundColor removes the glass
<TrueSheet backgroundColor="#ffffff" />

// ✅ v4 — same flat result
<TrueSheet backgroundColor="#ffffff" glass={false} />

// ✅ v4 — tinted glass
<TrueSheet backgroundColor="rgba(0, 122, 255, 0.25)" />

backgroundBlur still removes the glass on its own.

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.

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.

Step-by-Step Migration

  1. Update React Native to 0.82 or newer.
  2. Update the package:
    npm install @lodev09/react-native-true-sheet@^4.0.0
  3. Clean and reinstall iOS dependencies:
    cd ios
    rm -rf Pods Podfile.lock build
    pod install
    cd ..
  4. Clean Android build:
    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
    • Add glass={false} to sheets with a translucent backgroundColor that should stay flat on iOS 26+
    • Rename anchor to placement and anchorOffset to placementOffset, with "left" / "right" becoming "leading" / "trailing"
    • Replace FullWindowOverlay / Modal overlay workarounds with TrueSheetOverlay
    • If using the Sheet Navigator, install standard-navigation and update @react-navigation/native to 7.3+
    • If using Expo Router, install standard-navigation, replace the withLayoutContext wrapper with the Sheet layout from /navigation/expo-router, and update useTrueSheetNavigation imports
  6. Test your app:
    npx react-native run-ios
    npx react-native run-android

Need Help?

If you encounter any issues during migration:

Edit on GitHub

On this page