Bottom Sheet Expo Module#
A custom Expo module that provides native bottom sheet functionality for iOS and Android, using platform-specific native bottom sheet implementations (UISheetPresentationController on iOS, Material BottomSheetDialog on Android).
Overview#
This module wraps native bottom sheet components to provide a React Native interface with cross-platform consistency. It uses native presentation APIs rather than JavaScript-based animations for better performance and native behavior.
Key features:
- Native bottom sheet presentation on iOS and Android
- Automatic content height detection (no JS bridge round-trip)
- Configurable snap points (hidden, partial, full)
- Drag-to-dismiss with prevention controls
- Portal-based rendering for proper z-index layering
- Edge-to-edge support on modern Android versions
- iOS 26+ zoom transition support
Platform Support#
- iOS: Uses
UISheetPresentationController(iOS 15+) - Android: Uses Material Design
BottomSheetDialogwithBottomSheetBehavior - Web: Not supported (throws error)
Architecture#
TypeScript Layer#
The module exposes a React component that handles rendering and state management:
- BottomSheet.tsx (Native): Main component wrapping the native view
- BottomSheet.web.tsx (Web): Stub that throws an error
- BottomSheetNativeComponent.tsx: React wrapper with portal integration
- BottomSheetPortal.tsx: Portal system for rendering sheets above app content
- Portal.tsx: Generic portal implementation for managing component hierarchy
The component uses a class-based approach to expose imperative methods (present(), dismiss(), dismissAll()).
Native Layer#
iOS Implementation#
- BottomSheetModule.swift: Expo module definition with event handlers and prop bindings
- SheetView.swift: Main view component that creates and manages
SheetViewController- Observes content height via KVO (Key-Value Observing) on bounds
- Manages sheet lifecycle and state transitions
- Implements
UISheetPresentationControllerDelegatefor drag events
- SheetViewController.swift: UIViewController subclass with sheet presentation
- Configures detents (snap points) based on content height
- Handles iOS 26+ safe area adjustments for floating sheet style
- Animates detent changes when content resizes
- SheetManager.swift: Singleton that tracks all active sheets with weak references
- Util.swift: Helper for calculating screen height minus safe area insets
Android Implementation#
- BottomSheetModule.kt: Expo module definition mirroring iOS functionality
- BottomSheetView.kt: Main view component managing Material BottomSheetDialog
- Uses
OnLayoutChangeListenerto observe content height natively - Configures
BottomSheetBehaviorfor drag and snap behavior - Handles edge-to-edge display across Android versions (API 29-35+)
- Preserves status/nav bar appearance from host activity
- Uses
- DialogRootViewGroup.kt: Custom ViewGroup acting as RootView for the dialog
- Forwards touch events to React Native event system
- Updates shadow node size to match window dimensions
- Based on React Native's ReactModalHostView pattern
- SheetManager.kt: Singleton for tracking sheets (same pattern as iOS)
Content Height Detection#
Both platforms detect content height changes natively without JS bridge round-trips:
- iOS: KVO observation on the content view's
boundsproperty - Android:
OnLayoutChangeListeneron child views (catches React Native's directlayout()calls)
This eliminates layout jank when content changes (e.g., keyboard appearance, dynamic content loading).
Props#
interface BottomSheetViewProps {
children: React.ReactNode
// Appearance
cornerRadius?: number
backgroundColor?: ColorValue
containerBackgroundColor?: ColorValue
// Behavior
preventDismiss?: boolean // Disable swipe-to-dismiss
preventExpansion?: boolean // Lock to initial height (no full-screen)
disableDrag?: boolean // Disable drag handle (Android only)
fullHeight?: boolean // Start at full screen height
// Height constraints
minHeight?: number // Minimum height in dp
maxHeight?: number // Maximum height in dp
// iOS 26+ transition
sourceViewTag?: number // View tag for zoom transition origin
// Events
onAttemptDismiss?: (event: BottomSheetAttemptDismissEvent) => void
onSnapPointChange?: (event: BottomSheetSnapPointChangeEvent) => void
onStateChange?: (event: BottomSheetStateChangeEvent) => void
}
States and Snap Points#
States#
closed: Sheet is dismissedclosing: Sheet is animating closedopen: Sheet is fully visibleopening: Sheet is animating open
Snap Points#
Hidden(0): DismissedPartial(1): Half-expanded / content heightFull(2): Expanded to screen height
Usage#
Basic Example#
import {BottomSheet, BottomSheetProvider, BottomSheetOutlet} from '@modules/bottom-sheet'
// In your app root:
function App() {
return (
<BottomSheetProvider>
<YourApp />
<BottomSheetOutlet />
</BottomSheetProvider>
)
}
// In a component:
function MyComponent() {
const sheetRef = useRef<BottomSheet>(null)
const openSheet = () => {
sheetRef.current?.present()
}
const closeSheet = () => {
sheetRef.current?.dismiss()
}
return (
<>
<Button onPress={openSheet} title="Open Sheet" />
<BottomSheet
ref={sheetRef}
cornerRadius={16}
backgroundColor="white"
onStateChange={(e) => console.log(e.nativeEvent.state)}
>
<View style={{padding: 20}}>
<Text>Sheet content</Text>
<Button onPress={closeSheet} title="Close" />
</View>
</BottomSheet>
</>
)
}
Nested Sheets#
The module supports nesting sheets by using BottomSheetPortalProvider within sheet content:
<BottomSheet ref={outerSheetRef}>
<BottomSheetPortalProvider>
<Button onPress={() => innerSheetRef.current?.present()} />
<BottomSheet ref={innerSheetRef}>
<Text>Inner sheet content</Text>
</BottomSheet>
</BottomSheetPortalProvider>
</BottomSheet>
Dismiss All Sheets#
import {BottomSheetNativeComponent} from '@modules/bottom-sheet'
BottomSheetNativeComponent.dismissAll()
Key Implementation Details#
iOS Specific#
-
iOS 15 Compatibility: On iOS 15, custom detents are not available, so the module uses
.medium()detent and applies extra styling to prevent visual issues. -
iOS 26+ Zoom Transitions: When
sourceViewTagis provided on iOS 26+, the sheet zooms from the specified view. -
Detent Selection: The module automatically chooses between custom detents,
.medium(), and.large()based on content height and screen size.
Android Specific#
-
Edge-to-Edge: The module handles edge-to-edge display correctly across API levels:
- API 35+: Mandatory edge-to-edge
- API 30-34: Uses
currentWindowMetrics - API <30: Uses deprecated
getRealSize()
-
Status/Nav Bar Appearance: Preserves light/dark appearance from the host activity and reapplies it to the sheet dialog.
-
Drag Handling: On full-height sheets with
preventDismiss, dragging is disabled to prevent accidental dismissal (since there's no half-expanded snap point to land on). -
Layout Updates During Gestures: Content height changes are deferred during drag gestures to prevent fighting the user's input.
Platform Differences#
- cornerRadius: Applied to sheet on iOS, to content wrapper on Android (Android clips with
overflow: hidden) - disableDrag: Android-only prop (iOS drag behavior is controlled via
preventDismiss+preventExpansion) - sourceViewTag: iOS 26+ only (ignored on Android)
Files Reference#
TypeScript#
index.ts- Public API exportssrc/BottomSheet.types.ts- TypeScript type definitionssrc/BottomSheet.tsx- Native component (re-export)src/BottomSheet.web.tsx- Web stubsrc/BottomSheetNativeComponent.tsx- Native wrapper with portal integrationsrc/BottomSheetNativeComponent.web.tsx- Web stub for native componentsrc/BottomSheetPortal.tsx- Portal context and providerssrc/lib/Portal.tsx- Generic portal implementation
iOS#
ios/BottomSheetModule.swift- Module definitionios/SheetView.swift- Main view implementationios/SheetViewController.swift- View controller for sheet presentationios/SheetManager.swift- Singleton for tracking active sheetsios/Util.swift- Screen height utility
Android#
android/src/main/java/expo/modules/bottomsheet/BottomSheetModule.kt- Module definitionandroid/src/main/java/expo/modules/bottomsheet/BottomSheetView.kt- Main view implementationandroid/src/main/java/expo/modules/bottomsheet/DialogRootViewGroup.kt- Dialog root view groupandroid/src/main/java/expo/modules/bottomsheet/SheetManager.kt- Sheet tracking singleton
Configuration#
expo-module.config.json- Expo module configuration