npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

jam-bottomsheet

v0.1.14

Published

A lightweight and highly customizable React Native bottom sheet

Readme

jam-bottomsheet

npm version npm downloads license

A lightweight and highly customizable React Native bottom sheet built with:

  • react-native-reanimated
  • react-native-gesture-handler
  • react-native-keyboard-controller
  • react-native-worklets

Designed for smooth gestures, keyboard-aware interactions, expandable snap points, fullscreen presentations, and seamless nested scrolling.


Features

  • Smooth UI-thread animations powered by Reanimated
  • Expandable and fullscreen modes
  • Keyboard-aware behavior and animations
  • Nested scrolling support with gesture conflict handling
  • Supports both controlled and imperative APIs
  • Built-in backdrop support
  • Built-in safe area handling
  • Highly customizable appearance and behavior
  • Optimized with Reanimated worklets
  • End-to-end tested with Maestro

Installation

npm install jam-bottomsheet

Install peer dependencies if you don't already have them:

npm install react-native-reanimated react-native-gesture-handler react-native-safe-area-context react-native-keyboard-controller react-native-worklets

Setup

Provider Setup

Wrap your application with BottomSheetProvider:

import { BottomSheetProvider } from 'jam-bottomsheet';

export default function App() {
  return (
    <BottomSheetProvider>
      {/* app */}
    </BottomSheetProvider>
  );
}

React Native Reanimated

Add the Reanimated babel plugin:

module.exports = {
  presets: ['module:@react-native/babel-preset'],
  plugins: ['react-native-reanimated/plugin'],
};

Basic Usage

import React, { useState } from 'react';
import { Button, Text, View } from 'react-native';

import {
  BottomSheet,
} from 'jam-bottomsheet';

export default function App() {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <View style={{ flex: 1 }}>
      <Button
        title="Open Bottom Sheet"
        onPress={() => setIsOpen(true)}
      />

      <BottomSheet 
        isOpen={isOpen} 
        onCloseAnimationFinished={() => setIsOpen(false)}
      >
        <Text>Hello from the bottom sheet</Text>
      </BottomSheet>
    </View>
  );
}

Controlled vs Imperative API

jam-bottomsheet supports both declarative and imperative usage patterns.

Use the controlled API (isOpen and onCloseAnimationFinished) for state-driven interfaces and dynamic content.

Use the imperative API (open() and close()) for maximum responsiveness and direct control over presentation.


Usage with Imperative API

Animation Hooks

The imperative API methods open() and close() accept an optional hooks object.

export type BottomSheetAnimationCallback = () => void;

export interface BottomSheetAnimationHooks {
  onStarted?: BottomSheetAnimationCallback;
  onFinished?: BottomSheetAnimationCallback;
}

export interface BottomSheetRef {
  open: (hooks?: BottomSheetAnimationHooks) => void;
  close: (hooks?: BottomSheetAnimationHooks) => void;
}

Example

import React, { useState } from 'react';
import { Button, Text, View } from 'react-native';

import {
  BottomSheet,
  type BottomSheetRef,
} from 'jam-bottomsheet';

export default function App() {
  const bottomSheetRef = React.useRef<BottomSheetRef>(null);

  const openBottomSheet = () => {
    bottomSheetRef.current?.open({
      onStarted: () => console.log("Bottom Sheet started to open"),
      onFinished: () => console.log("Bottom Sheet is fully opened"),
    });
  };

  const closeBottomSheet = () => {
    bottomSheetRef.current?.close({
      onStarted: () => console.log("Bottom Sheet started to close"),
      onFinished: () => console.log("Bottom Sheet is fully closed"),
    });
  };

  return (
    <View style={{ flex: 1 }}>
      <Button
        title="Open Bottom Sheet"
        onPress={openBottomSheet}
      />

      <Button
        title="Close Bottom Sheet"
        onPress={closeBottomSheet}
      />

      <BottomSheet imperative ref={bottomSheetRef}>
        <Text>Hello from the bottom sheet</Text>
      </BottomSheet>
    </View>
  );
}

Loading state-driven content

The imperative API allows the bottom sheet to start animating immediately, without waiting for a React re-render.

When the sheet content depends on state updates, the animation may begin before the new content is rendered. This can cause visual inconsistencies during the transition, such as content appearing midway through the opening animation.

To coordinate rendering with the animation lifecycle, you can use animation hooks to delay the state update until the opening animation finishes.

import React, { useState } from "react";
import { FlatList, Pressable, Text } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import { BottomSheetRef, BottomSheet } from "jam-bottomsheet";

const posts = [
  {
    id: 1,
    title: "React Native"
  },
  // ...
]

export default function App() {
  const [post, setPost] = useState<(typeof posts)[number] | undefined>();
  const bottomSheetRef = React.useRef<BottomSheetRef>(null);

  const detailPost = (post: (typeof posts)[number]) => {
    bottomSheetRef.current?.open({
      onFinished: () => setPost(post),
    });
  };

  return (
    <SafeAreaView style={{ flex: 1 }}>
      <FlatList
        data={posts}
        renderItem={({ item }) => {
          return (
            <Pressable onPress={() => detailPost(item)}>
              <Text>{item.title}</Text>
            </Pressable>
          );
        }}
      />

      <BottomSheet 
        imperative 
        ref={bottomSheetRef} 
        onCloseAnimationFinished={() => setPost(undefined)}
      >
        {
          post ? 
            <Text>{post.title}</Text> : 
            <Text>Post is loading...</Text>
        }
      </BottomSheet>
    </SafeAreaView>
  );
}

Expandable Bottom Sheet

<BottomSheet
  isOpen={isOpen} 
  onCloseAnimationFinished={() => setIsOpen(false)}  
  expandable
  snapPointsCollapsed={300}
  snapPointsExpanded={700}
>
  {/* content */}
</BottomSheet>

Swipe up to expand and swipe down to collapse.


Fullscreen Bottom Sheet

<BottomSheet
  isOpen={isOpen} 
  onCloseAnimationFinished={() => setIsOpen(false)}
  fullscreen
>
  {/* content */}
</BottomSheet>

Props

| Prop | Type | Required | Default | Description | |---|---|---|---|---| | imperative | boolean | | false | Enables imperative control via refs instead of declarative state-based control | | isOpen | boolean | Required when imperative is false | false | Determines if the sheet is open | | expandable | boolean | | false | Enables expand/collapse behavior | | fullscreen | boolean | | false | Makes the sheet fullscreen | | snapPointsCollapsed | number | | 400 | Height when collapsed | | snapPointsExpanded | number | | screen height | Height when expanded | | backdropOpacity | number | | 0.5 | Backdrop opacity | | backdropColor | string | | #000000 | Backdrop color | | backgroundColor | string | | #ffffff | Sheet background color | | borderRadius | number | | 24 | Top border radius | | handleColor | string | | #000000 | Handle color | | hideHandle | boolean | | false | Hides the handle | | panSnapPoints | number | | 100 | Gesture threshold | | animationDuration | number | | 300 | Animation duration | | closeOnBackdropTap | boolean | | true | Close when backdrop is tapped | | dismissKeyboardOnClose | boolean | | true | Dismiss keyboard on close | | captureGestureOnScrollStart | boolean | | true | Allows the sheet to capture downward gestures when the internal scroll view is at the top | | captureGestureOnScrollEnd | boolean | | true | Allows the sheet to capture upward gestures when the internal scroll view is at the bottom | | onOpenAnimationStarted | () => void | | undefined | Called when open animation starts | | onOpenAnimationFinished | () => void | | undefined | Called when open animation is finished | | onCloseAnimationStarted | () => void | | undefined | Called when close animation starts | | onCloseAnimationFinished | () => void | Required when imperative is false | undefined | Called when close animation is finished | | scrollViewContentContainerStyle | ViewStyle | | undefined | ScrollView content style |


Notes

  • The bottom sheet should generally be rendered outside safe area containers, as it already handles safe area boundaries internally
  • It is recommended to use portals such as @gorhom/portal to avoid visual inconsistencies caused by parent layouts, such as safe areas, clipping, or stacking issues
  • When using the imperative API with state-driven content, avoid changing the rendered content while the sheet is animating to prevent visual inconsistencies during the transition
  • Content is rendered inside an internal ScrollView
  • Keyboard appearance and dismissal animations are handled automatically
  • Gesture conflicts with nested scrolling are handled internally
  • Works well with forms and text inputs

License

MIT