AniUI

BottomSheet

Bottom sheet overlay powered by @gorhom/bottom-sheet.

Web preview — components render natively on iOS & Android
import { BottomSheet } from "@/components/ui/bottom-sheet";
import { useRef } from "react";
import GorhomBottomSheet from "@gorhom/bottom-sheet";

export function MyScreen() {
  const sheetRef = useRef<GorhomBottomSheet>(null);
  return (
    <>
      <Button onPress={() => sheetRef.current?.expand()}>
        Open Sheet
      </Button>
      <BottomSheet ref={sheetRef} snapPoints={["25%", "50%"]}>
        <Text>Sheet content goes here</Text>
      </BottomSheet>
    </>
  );
}

Installation#

npx @aniui/cli add bottom-sheet

This component requires additional dependencies:

npx expo install @gorhom/bottom-sheet react-native-gesture-handler react-native-reanimated

You also need to wrap your app with GestureHandlerRootView from react-native-gesture-handler.

Usage#

app/index.tsx
import { BottomSheet } from "@/components/ui/bottom-sheet";
import { useRef } from "react";
import GorhomBottomSheet from "@gorhom/bottom-sheet";

export function MyScreen() {
  const sheetRef = useRef<GorhomBottomSheet>(null);
  return (
    <>
      <Button onPress={() => sheetRef.current?.expand()}>
        Open Sheet
      </Button>
      <BottomSheet ref={sheetRef} snapPoints={["25%", "50%"]}>
        <Text>Sheet content goes here</Text>
      </BottomSheet>
    </>
  );
}

Props#

PropTypeDefault
snapPoints
(string | number)[]
["25%", "50%"]
className
string
—
children
React.ReactNode
required

Also accepts all @gorhom/bottom-sheet props. Use a ref to control the sheet imperatively with expand(), close(), and snapToIndex().

Accessibility#

  • Powered by @gorhom/bottom-sheet with backdrop dismiss.
  • Sheet content is focusable and supports screen reader navigation.

Source#

components/ui/bottom-sheet.tsx
import React, { forwardRef, useCallback } from "react";
import { View } from "react-native";
import {
  BottomSheetModal,
  BottomSheetBackdrop,
  BottomSheetView,
} from "@gorhom/bottom-sheet";
import { cn } from "@/lib/utils";
import { useThemeColors } from "@/components/ui/theme-provider";
import { useFoldFeatures } from "@/components/ui/fold";

export interface BottomSheetProps {
  className?: string;
  children: React.ReactNode;
  snapPoints?: (string | number)[];
  /**
   * On an unfolded iPhone Duo / foldable, clamp the sheet to one pane instead
   * of spanning the full screen width. Defaults to "left" (the OS treats the
   * left pane as primary). Set to "full" to opt out and keep the pre-fold
   * screen-wide behavior. Ignored on non-fold devices.
   */
  foldPane?: "left" | "right" | "full";
}

export const BottomSheet = forwardRef<BottomSheetModal, BottomSheetProps>(
  ({ className, children, snapPoints = ["25%", "50%"], foldPane = "left", ...props }, ref) => {
    const colors = useThemeColors();
    const fold = useFoldFeatures();
    const renderBackdrop = useCallback(
      (backdropProps: React.ComponentProps<typeof BottomSheetBackdrop>) => (
        <BottomSheetBackdrop {...backdropProps} disappearsOnIndex={-1} appearsOnIndex={0} opacity={0.5} />
      ),
      []
    );

    // Fold-aware width clamp: on an unfolded Duo, inherit one pane's width
    // (foldX, which is window.width / 2). Pre-fold / non-Duo renders behave
    // identically to today because fold.foldX is null.
    const foldClamp =
      fold.foldX && foldPane !== "full"
        ? { width: fold.foldX, left: foldPane === "right" ? fold.foldX : 0 }
        : undefined;

    return (
      <BottomSheetModal
        ref={ref}
        snapPoints={snapPoints}
        enablePanDownToClose
        backdropComponent={renderBackdrop}
        backgroundStyle={{ backgroundColor: colors.background }}
        handleIndicatorStyle={{ backgroundColor: colors.mutedForeground }}
        containerStyle={foldClamp}
        {...props}
      >
        <BottomSheetView>
          <View className={cn("p-4", className)}>{children}</View>
        </BottomSheetView>
      </BottomSheetModal>
    );
  }
);

BottomSheet.displayName = "BottomSheet";