React Native Bottom Sheet

repository·main·Indexed 19 days ago

https://github.com/software-mansion-labs/react-native-bottom-sheet

A high-performance library by Software Mansion providing specialized bottom-sheet UI components for React Native. It features a hybrid implementation model where gestures, snapping, and scroll negotiation are handled in native code, eliminating dependencies on Reanimated or React Native Gesture Handler. The library offers BottomSheet for inline layouts and ModalBottomSheet for overlay content, supporting numeric, percentage, and content-based detents.

Tokens
13.6K
Snippets
34
Records
56
Agent score
60%

What's inside @swmansion/react-native-bottom-sheet

  1. Overview of React Native Bottom Sheet

    main

    React Native Bottom Sheet is a high-performance library providing bottom-sheet components for React Native. It features a native implementation to ensure optimal performance and supports both inline and modal sheet components. Key capabilities include:

    • Flexible Surfaces: You can bring your own sheet surface.
    • Dynamic Sizing: Supports content-based sizing out of the box.
    • Scroll Management: Automatically handles vertically scrollable children.
    • UI Synchronization: Provides position tracking to drive other UI elements tied to the sheet's state.
    • Advanced Detents: Supports programmatic-only detents for snap points that cannot be reached via manual dragging.
  2. Implement content positioning with bottom padding

    main

    To ensure consistent behavior across Android and iOS, use bottom padding for the content-region inset rather than top padding.

    • Android: The host natively places children inside the offset/translated container, ignoring Yoga origins.
    • iOS: Fabric applies Yoga frames (including origin) directly to child views.

    Using top padding causes a 'double-offset' effect on iOS, where content is pushed twice (once by the container offset and once by the Yoga origin). Using bottom padding ensures that contentOffsetY correctly carries the container's top offset (containerTop + translationY) on both platforms without shifting the content out of the visible surface.

  3. Optimize `adopt()` for high-frequency state updates

    main

    In the Fabric architecture, the adopt() method is called during every component creation and every clone. When performing animations that push per-frame state updates, adopt() runs at the frame rate. To ensure smooth performance and avoid UI jank, ensure that any logic inside adopt() is:

    1. Cheap: Minimize computational complexity.
    2. Idempotent: The method must be safe to call repeatedly with the same state without side effects.

    Anything set from state during adopt() is re-stamped per commit.

  4. Understand Yoga layout behavior for absolute positioning

    main

    When configuring layouts using Yoga (the layout engine used by React Native):

    • Absolutely positioned children: These ignore the parent's padding and position themselves against the padding box's top edge (the border edge).
    • In-flow children: These lay out within the parent's padding.

    This distinction allows you to use setPadding to cap a content region without affecting the outer surface of the component.

  5. Understand and configure Detents

    main

    Detents define the snap points (heights) of the bottom sheet. They can be specified in three ways:

    1. Numeric: A fixed height in density-independent layout units (points on iOS, dp on Android). Example: 300.
    2. Percentage: A string representing a percentage of the available sheet height (e.g., '30%', '12.5%'). Percentages must be unsigned and contain no whitespace.
    3. 'content': A special string that sets the detent to the measured height of the sheet's children (capped by available height).

    Rules for Detents:

    • Ordering: Must be passed in ascending order (shortest to tallest). The library validates this during native layout.
    • Validation: Numeric detents must be finite and non-negative. Passing -10, NaN, or Infinity will throw an Error.
    • Layout: Sheet children are laid out in a flex container. To ensure content fills the sheet when using the 'content' detent, apply flex: 1 to your content container. Do not apply flex: 1 to the surface prop; the library manages the surface size.
    • Full Screen: By default, full-height detents are capped below the status bar. Use the extendUnderStatusBar prop to allow the sheet to occupy the full screen height.
    <BottomSheet
      // Example: Responsive heights
      detents={['0%', '30%', '80%']}
      // Example: Fixed and content heights
      detents={[0, 300, 'content']}
      index={index}
      onIndexChange={setIndex}
      surface={
        <View style={[StyleSheet.absoluteFill, { backgroundColor: 'white' }]} />
      }
    >
      <View style={{ flex: 1 }}>{/* Full-height sheet content. */}</View>
    </BottomSheet>
  6. How Modal and Screen sizing works (the adopt pattern)

    main

    To ensure content inside a <Modal> or a react-native-screens container is sized correctly, the architecture uses a "state → adopt()" pattern.

    1. Native Measurement: The native view reports its measured geometry (e.g., onSizeChanged or onLayout) into a state object.
    2. The adopt() Method: The C++ component descriptor's adopt() method runs on every create/clone. It takes the dimensions from the state and forces the Yoga shadow node's size using setSize() and sets the position type to YGPositionTypeAbsolute.

    This allows children to mount with correct, non-zero frames immediately, even without a traditional Android layout traversal.

  7. Avoid side effects during iOS Fabric view reparenting

    main

    On iOS, Fabric may transiently detach views from the window (window == nil) during a commit (e.g., when changing the sheet index or pointerEvents). This is often a reparenting operation, not a true removal from the screen.

    Best Practices:

    • Geometry/Layout: Do not run geometry refreshes (refreshDetentsFromLayout, layoutSubviews) while hasLaidOut && window == nil. Re-run them in didMoveToWindow when the view reattaches.
    • Animations: Do not trigger animations or assume a view is gone just because window is nil.
    • Teardown: If you need to tear down an overlay, defer the operation by one runloop turn using dispatch_async to the main queue and re-check if the window has returned to avoid breaking mid-flight animations.
  8. Choose a placement mode for your bottom sheet

    main

    React Native Bottom Sheet provides three distinct placement modes depending on how you want the sheet to interact with your view hierarchy and layering:

    1. BottomSheet (Inline): Renders directly within your screen's React Native hierarchy. Use this when the sheet needs to be layered alongside nearby content or constrained by the surrounding layout.
    2. ModalBottomSheet (Portal-based): Uses the BottomSheetProvider's React Native portal by default. This is ideal for modal behavior where you still want to control layering within your React tree (e.g., ensuring toasts or menus appear above or below the sheet based on their position relative to the provider).
    3. ModalBottomSheet with nativeOverlay: Bypasses the provider portal to present in a native overlay. Use this when the sheet must appear above other native overlays, such as a React Navigation native-stack screen presented with presentation: 'modal'.
  9. Compare React Native Bottom Sheet with other libraries

    main

    vs @gorhom/bottom-sheet

    While both offer similar functionality (detents, dynamic sizing, scroll coordination), @gorhom/bottom-sheet relies on Reanimated and React Native Gesture Handler. React Native Bottom Sheet moves the core mechanics (gestures, snapping, scroll negotiation) into native code to protect performance from JS thread congestion and allows the use of standard React Native scrollables.

    vs Expo UI / Expo Router / True Sheet

    Libraries like Expo UI or True Sheet lean into platform presentation APIs (system-style sheets). These are best for system-standard behavior but offer less control. React Native Bottom Sheet is a lower-level primitive that allows you to provide the complete surface in React and define custom behavior, such as programmatic-only snap points.

  10. Measure content height using a trailing marker View

    main

    To determine the height of 'content' detents (which requires knowing the children's flow height), use a trailing zero-size <View> marker appended after the children.

    Mechanism:

    1. Append a <View> as the last sibling in the content container.
    2. The position of this marker represents the total content height.
    3. Listen for native layout signals from this marker:
      • Android: Use OnLayoutChangeListener.
      • iOS: Use KVO on the marker layer's position or bounds.

    This approach avoids the performance overhead of per-child listeners while providing a reliable signal from the shadow tree to the native side.

  11. Understand the Fabric layout behavior on Android

    main

    When using React Native with the Fabric architecture on Android, layout is applied imperatively per view via SurfaceMountingManager.updateLayout.

    Key behaviors to note:

    • Layout Parameters: Fabric does not set LayoutParams; views retain the defaults generated by the parent at addView.
    • Custom Layouts: If a parent view manager returns true for needsCustomLayoutForChildren(), Fabric will measure the child but skip the layout() call, leaving placement to the native parent.
    • Layout Request Termination: In a Fabric subtree, ReactViewGroup.requestLayout() is a no-op terminator. It does not propagate layout requests. This means layout requests triggered inside a Fabric subtree will not trigger a native traversal of the parent hierarchy.
    • Hit-testing: Android hit-testing is based on real native bounds (view.width/height). A view with 0×0 dimensions cannot be touched, even if the C++ shadow tree suggests otherwise.