◉RORK LABJP
●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…●GPT-6.1 SOL — Added to Rork's model menu (09-29) with Low, Medium and High effort; included on Pro and Max●SONNET 5.5 — Claude Sonnet 5.5 is selectable: strong at UI design and faster than Sonnet 5 (09-28)●SDK 58 — Beta since 09/15 with no stable date yet; running projects in Expo Go has required login since 09/03●WIDGET — A write-up traces an iOS widget that renders black only in production builds●OBSERVE — EAS Observe now records native crashes too (SDK 57.0.21 or later, 10/07)●NEW — My App Open Ad Only Showed When the App…
Articles/Dev Tools
⬡ Dev Tools/2026-04-29Intermediate

Adding Bottom Sheets to a Rork App — A Practical Guide to @gorhom/bottom-sheet on iOS and Android

Rork's default Modal works for confirmations, but the moment you need multiple snap points or inertial scroll inside the sheet it falls short. This guide walks through dropping @gorhom/bottom-sheet into a Rork project, handling the keyboard, and smoothing out iOS/Android differences.

Rork585Bottom Sheet@gorhom/bottom-sheetReact Native239UI10Reanimated10Gesture Handler

You don't usually need a custom bottom sheet in a Rork-generated app — until suddenly you do. A map screen with a sliding place card, a filter panel that peeks above a product list, a half-height form that you don't want to hide the screen behind: these are moments where a plain Modal isn't enough, and the gap between "I just want a sheet" and "I have a sheet that feels native on both platforms" turns out to be surprisingly wide.

Personally, I lost half a day trying to make Rork's Modal output behave like a two-step sheet, only to find that inertial scroll inside the sheet wasn't kicking in and the Android backdrop was rendering as solid black. The cleanest answer ended up being to stop fighting it and drop in @gorhom/bottom-sheet as a layered library. This guide is the version of the notes I wish I had on day one.

Why Rork's default Modal eventually runs out of room

Rork tends to emit React Native's standard Modal for sheet-ish UI. That's perfectly fine for a confirmation dialog or a small form. The friction starts when you want any of the following:

  • Multiple snap points (collapsed, half, full)
  • A scrollable list inside the sheet with proper inertia
  • Drag-to-resize gestures that work alongside the inner scroll
  • A backdrop fade and rounded corners that match iOS conventions

You can technically build all of this on top of Modal with PanResponder and a hand-rolled Animated.Value, but the maintenance cost adds up fast — separating gesture priority between the outer drag and the inner list is especially painful. Pulling in a focused library is the more honest tradeoff.

Adding @gorhom/bottom-sheet to a Rork project

Rork projects use Expo + React Native under the hood. @gorhom/bottom-sheet depends on Reanimated and Gesture Handler, so the cleanest approach is to confirm those are installed before pulling in the sheet itself.

# Run from the root of your Rork project
npx expo install react-native-reanimated react-native-gesture-handler
npm install @gorhom/bottom-sheet

Reanimated requires a Babel plugin, and the order matters — it has to be the last entry in the plugins array, or you'll get cryptic build errors that are hard to attribute back to the right cause.

// babel.config.js
module.exports = function (api) {
  api.cache(true);
  return {
    presets: ['babel-preset-expo'],
    plugins: [
      // ...other plugins...
      'react-native-reanimated/plugin', // must be last
    ],
  };
};

Gesture Handler needs a GestureHandlerRootView wrapping the entire app. In a Rork project that usually means adding a single line to the generated _layout.tsx.

// app/_layout.tsx
import { GestureHandlerRootView } from 'react-native-gesture-handler';
import { Slot } from 'expo-router';
 
export default function RootLayout() {
  return (
    <GestureHandlerRootView style={{ flex: 1 }}>
      <Slot />
    </GestureHandlerRootView>
  );
}

That's the entire setup. We've barely touched what Rork generated.

The smallest sheet that actually does something

Let's start with a sheet that has two snap points — half open and fully open — and a button that triggers it. Holding a useRef to the sheet so you can call expand() or close() from anywhere makes it easy to plug into screens generated later by the AI.

import React, { useCallback, useMemo, useRef } from 'react';
import { View, Text, Button, StyleSheet } from 'react-native';
import BottomSheet, { BottomSheetView } from '@gorhom/bottom-sheet';
 
export default function PlaceDetailScreen() {
  const sheetRef = useRef<BottomSheet>(null);
 
  // Use percentages so the layout adapts across device sizes
  const snapPoints = useMemo(() => ['25%', '60%', '90%'], []);
 
  const openSheet = useCallback(() => {
    sheetRef.current?.snapToIndex(1); // open at the 60% position
  }, []);
 
  return (
    <View style={styles.container}>
      <Button title="Show details" onPress={openSheet} />
 
      <BottomSheet
        ref={sheetRef}
        index={-1} // start closed
        snapPoints={snapPoints}
        enablePanDownToClose
        backgroundStyle={styles.sheetBg}
        handleIndicatorStyle={styles.indicator}
      >
        <BottomSheetView style={styles.sheetBody}>
          <Text style={styles.title}>Cafe Hirokawa</Text>
          <Text>Hours: 10:00 - 19:00</Text>
          <Text>Shibuya, Tokyo</Text>
        </BottomSheetView>
      </BottomSheet>
    </View>
  );
}
 
const styles = StyleSheet.create({
  container: { flex: 1, padding: 24 },
  sheetBg: { backgroundColor: '#ffffff' },
  indicator: { backgroundColor: '#cccccc', width: 48 },
  sheetBody: { padding: 20 },
  title: { fontSize: 20, fontWeight: '600', marginBottom: 8 },
});

Tapping the button slides the sheet up to 60%; the user can drag it to 90%, back down to 25%, or off-screen entirely. Without enablePanDownToClose, the dismiss gesture quietly stops working — that's an easy one to forget when you're iterating.

Designing the snap points

Snap points read better as percentages than as pixel heights. iPhone SE and iPad need to feel right with the same configuration, and percentages adapt without you having to reach for a Dimensions.get() calculation. If your sheet contains a fixed-height card, you can also pass 'CONTENT_HEIGHT' (v4+) and let the library size itself.

Adding more snap points is tempting, but I've found three is usually the ceiling — anything more and users start to feel like the sheet is "fighting them" mid-drag. The exception is map-style apps where Apple Maps' minimal handle position (around 10%) makes sense as a fourth stop.

If you need to react to the user's position — focus an input when the sheet is fully open, for example — onChange gives you the index. Just remember any side effect that touches React state has to be wrapped in runOnJS, since the gesture handlers run on Reanimated's worklet thread. I dug into that pattern in more depth in Advanced animations in Rork with Reanimated and Gesture Handler.

Forms inside a sheet, and the keyboard problem

The first time you put a TextInput in a bottom sheet, the keyboard will hide the field. @gorhom/bottom-sheet ships a BottomSheetTextInput specifically for this — it tracks focus and adjusts the sheet's offset automatically.

import BottomSheet, {
  BottomSheetView,
  BottomSheetTextInput,
} from '@gorhom/bottom-sheet';
 
<BottomSheet
  ref={sheetRef}
  index={-1}
  snapPoints={['40%', '90%']}
  keyboardBehavior="interactive" // iOS: track keyboard naturally
  keyboardBlurBehavior="restore" // restore on dismiss
  android_keyboardInputMode="adjustResize"
>
  <BottomSheetView style={{ padding: 16 }}>
    <BottomSheetTextInput
      placeholder="Add a note..."
      style={{
        height: 44,
        paddingHorizontal: 12,
        backgroundColor: '#f4f4f4',
        borderRadius: 8,
      }}
    />
  </BottomSheetView>
</BottomSheet>

keyboardBehavior="interactive" is iOS-only and makes the sheet follow the keyboard smoothly. On Android you also have to set softwareKeyboardLayoutMode: "resize" in app.json; leave it on "pan" and the entire sheet will get pushed up out of position when the keyboard opens.

If you have multiple inputs and notice a beat of lag when switching focus, swapping the inner container for BottomSheetScrollView usually smooths it out — and you get correct scroll-inside-sheet behavior as a bonus.

Where iOS and Android quietly disagree

Once you start testing on real devices, the OS-level differences show up faster than you'd expect. Three I've personally tripped on:

  • Backdrop dimming. iOS fades the backdrop nicely on its own. Some Android devices render a solid black instead of a translucent overlay unless you pass BottomSheetBackdrop explicitly. Worth wiring up from day one.
  • Handle hit area. Android's default handle target is small, and fingers slip off. Bumping handleHeight to ~28-32 makes the drag feel intentional.
  • Hardware back button. On Android the user expects the back button to close just the sheet, not the screen. Register a BackHandler inside useFocusEffect, call sheetRef.current?.close(), and return true to consume the event.

None of these are things Rork's default output considers, so a quick pass on a mid-tier Android right after dropping in the sheet is well worth the time. If your sheet starts crashing only on device, How to debug a Rork app that crashes on a real device covers the diagnostic flow I usually run.

For a more polished feel, swap out the inner spinner for a skeleton during loading. The pattern I use is in Implementing skeleton screens in Rork for smoother loading, and it pairs naturally with a sheet that's already animating in.

Start small and add as you go

If Rork's default Modal is doing the job, leave it alone. The moment you reach for a half-open sheet or a list inside a draggable card, though, the cost-benefit shifts hard toward a dedicated library — and @gorhom/bottom-sheet slots into a Rork project without forcing a rewrite of what the AI generated.

A good first migration is to pick one screen where the win is obvious — a category picker that slides up from the bottom, a place detail card on a map — and replace just that one. Once you've felt how the gestures behave on real hardware, deciding where else to use it gets a lot easier.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • ✦Copy-paste ready implementation code
  • ✦New advanced guides published daily
  • ✦$5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

⬡ Dev Tools2026-09-04
EAS secret visibility does not keep a value out of your app — deciding prefix and visibility separately
The EXPO_PUBLIC_ prefix decides what ships inside your app; EAS visibility decides who can read it. Why stacking them blanks a value on OTA updates, and how to check your build.
⬡ Dev Tools2026-08-22
Every bulk replace exited zero. The damage was in the lines I did not delete
Run a bulk replace over generated code and the breakage lands on the neighbouring lines, not the matched ones. Here is what broke in a live project, and a dependency-free guard that checks the invariants a replace must preserve.
⬡ Dev Tools2026-08-14
Find the native edits expo prebuild will erase before you upgrade to SDK 57
Expo SDK 57 makes expo prebuild clear and regenerate ios and android by default. Here is how to audit your hand edits first, move them into config plugins, and why 57.0.9 matters for Reanimated apps.