├── .gitignore ├── README.md ├── apps ├── native │ ├── .gitignore │ ├── README.md │ ├── app-env.d.ts │ ├── app.json │ ├── app │ │ ├── _layout.tsx │ │ └── index.tsx │ ├── assets │ │ ├── fonts │ │ │ └── SpaceMono-Regular.ttf │ │ └── images │ │ │ ├── adaptive-icon.png │ │ │ ├── favicon.png │ │ │ ├── icon.png │ │ │ ├── partial-react-logo.png │ │ │ ├── react-logo.png │ │ │ ├── react-logo@2x.png │ │ │ ├── react-logo@3x.png │ │ │ └── splash.png │ ├── babel.config.js │ ├── global.css │ ├── index.js │ ├── metro.config.js │ ├── package.json │ ├── tailwind.config.js │ └── tsconfig.json └── web │ ├── .eslintrc.json │ ├── .gitignore │ ├── README.md │ ├── app │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx │ ├── next.config.js │ ├── package.json │ ├── postcss.config.mjs │ ├── public │ ├── next.svg │ └── vercel.svg │ ├── tailwind.config.ts │ └── tsconfig.json ├── github.gif ├── package.json ├── packages ├── app │ ├── index.ts │ ├── package.json │ ├── screens │ │ └── home.tsx │ └── tsconfig.json └── ui │ ├── bottom-sheet │ ├── index.tsx │ ├── index.web.tsx │ ├── types.tsx │ └── util.ts │ ├── index.ts │ ├── package.json │ ├── text │ └── index.tsx │ ├── tsconfig.json │ └── view │ └── index.tsx ├── tailwind.config.ts ├── tsconfig.json ├── turbo.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .yarn 2 | !.yarn/patches 3 | !.yarn/plugins 4 | !.yarn/releases 5 | !.yarn/sdks 6 | !.yarn/versions 7 | 8 | # Swap the comments on the following lines if you don't wish to use zero-installs 9 | # Documentation here: https://yarnpkg.com/features/zero-installs 10 | !.yarn/cache 11 | #.pnp.* 12 | 13 | .next/ 14 | .swc/ 15 | out/ 16 | build 17 | 18 | node_modules 19 | npm-debug.log 20 | yarn-error.log 21 | yarn-debug.log 22 | 23 | .env.local 24 | .env.development.local 25 | .env.test.local 26 | .env.production.local 27 | 28 | .turbo -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Universal Bottom Sheet 2 | A bottom sheet component that combines [Gorhom Bottom Sheet](https://github.com/gorhom/react-native-bottom-sheet) and [Vaul](https://github.com/emilkowalski/vaul) for seamless and responsive experience across both mobile and web. 3 | 4 | ![Bottom Sheet](./github.gif) 5 | 6 | > **Note:** This is not a standalone package. It is a wrapper around Gorhom Bottom Sheet and Vaul for use across both mobile and web. You need to install Gorhom Bottom Sheet and Vaul separately to use this component. 7 | 8 | 9 | ### Installation 10 | I recommend following the [installation guide](https://ui.gorhom.dev/components/bottom-sheet/) for Gorhom's Bottom Sheet 11 | 12 | ```bash 13 | npm install @gorhom/bottom-sheet@^4 14 | ``` 15 | or 16 | ```bash 17 | yarn add @gorhom/bottom-sheet@^4 18 | 19 | ``` 20 | ### Peer Dependencies 21 | Make sure to install the following peer dependencies 22 | ```bash 23 | npm install react-native-reanimated react-native-gesture-handler 24 | ``` 25 | or 26 | ```bash 27 | yarn add react-native-reanimated react-native-gesture-handler 28 | ``` 29 | or with expo 30 | ```bash 31 | npx expo install react-native-reanimated react-native-gesture-handler 32 | ``` 33 | ### Web Support 34 | Make sure to install the following dependencies 35 | ```bash 36 | npm install vaul 37 | ``` 38 | or 39 | ```bash 40 | yarn add vaul 41 | ``` 42 | 43 | ### Styling with NativeWind (Optional) 44 | 45 | This example uses [NativeWind](https://www.nativewind.dev/v4/overview/) for styling. You can use any other styling library of your choice. 46 | 47 | Follow the installation guide [here](https://www.nativewind.dev/v4/getting-started/react-native) 48 | 49 | 50 | ### Usage 51 | An example usage of component is shown below: 52 | 53 | The `ui/bottom-sheet` module exports the following components: 54 | 55 | - `BottomSheetModal` 56 | - `BottomSheetModalProvider` 57 | - `BottomSheetView` 58 | - `BottomSheetTrigger` 59 | - `BottomSheetHandle` 60 | - `BottomSheetScrollView` 61 | 62 | 63 | The files can be found in the [`ui/bottom-sheet`](https://github.com/adebayoileri/universal-bottom-sheet/tree/main/packages/ui/bottom-sheet) directory. Copy the files to your project and import them as shown below: 64 | 65 | ```tsx 66 | "use client"; 67 | 68 | import React, { useCallback, useMemo, useRef } from "react"; 69 | import { Text } from "ui/text"; 70 | import { View } from "ui/view"; 71 | import { 72 | BottomSheetModal, 73 | BottomSheetView, 74 | BottomSheetTrigger, 75 | BottomSheetHandle, 76 | } from "ui/bottom-sheet"; 77 | import { Pressable, Platform } from "react-native"; 78 | import { useSharedValue } from "react-native-reanimated"; 79 | 80 | export function Home() { 81 | const [isOpen, setIsOpen] = React.useState(false); 82 | 83 | const animatedIndex = useSharedValue(0); 84 | const animatedPosition = useSharedValue(0); 85 | // ref 86 | const bottomSheetModalRef = useRef(null); 87 | 88 | // bottomSheetModalRef 89 | console.log({ bottomSheetModalRef }); 90 | 91 | // variables 92 | const snapPoints = useMemo(() => [600, "20%", "50%", "70%", "95%"], []); 93 | 94 | // callbacks 95 | const handlePresentModalPress = useCallback(() => { 96 | // bottomSheetWebRef.current?.focus(); 97 | 98 | if (isOpen) { 99 | bottomSheetModalRef.current?.dismiss(); 100 | setIsOpen(false); 101 | } else { 102 | bottomSheetModalRef.current?.present(); 103 | setIsOpen(true); 104 | } 105 | }, [isOpen]); 106 | 107 | const handleSheetChanges = useCallback((index: number) => { 108 | console.log("handleSheetChanges", index); 109 | }, []); 110 | 111 | return ( 112 | 113 | 114 | {Platform.OS !== "web" && ( // Use this condition if you want to control the modal from outside for only mobile 115 | 116 | Present Modal 117 | 118 | )} 119 | 120 | ( 127 | 132 | )} 133 | > 134 | {Platform.OS === "web" && ( 135 | <> 136 | 137 | Present Modal 138 | 139 | 140 | )} 141 | 142 | {Platform.OS === "web" && ( 143 | 148 | )} 149 | Awesome 🎉 150 | 151 | 152 | 153 | 154 | ); 155 | } 156 | ``` 157 | ### Author 158 | 159 | #### [Adebayo Ilerioluwa](https://github.com/adebayoileri) -------------------------------------------------------------------------------- /apps/native/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | dist/ 4 | npm-debug.* 5 | *.jks 6 | *.p8 7 | *.p12 8 | *.key 9 | *.mobileprovision 10 | *.orig.* 11 | web-build/ 12 | 13 | # macOS 14 | .DS_Store 15 | 16 | # @generated expo-cli sync-2b81b286409207a5da26e14c78851eb30d8ccbdb 17 | # The following patterns were generated by expo-cli 18 | 19 | expo-env.d.ts 20 | # @end expo-cli -------------------------------------------------------------------------------- /apps/native/README.md: -------------------------------------------------------------------------------- 1 | # Welcome to your Expo app 👋 2 | 3 | This is an [Expo](https://expo.dev) project created with [`create-expo-app`](https://www.npmjs.com/package/create-expo-app). 4 | 5 | ## Get started 6 | 7 | 1. Install dependencies 8 | 9 | ```bash 10 | npm install 11 | ``` 12 | 13 | 2. Start the app 14 | 15 | ```bash 16 | npx expo start 17 | ``` 18 | 19 | In the output, you'll find options to open the app in a 20 | 21 | - [development build](https://docs.expo.dev/develop/development-builds/introduction/) 22 | - [Android emulator](https://docs.expo.dev/workflow/android-studio-emulator/) 23 | - [iOS simulator](https://docs.expo.dev/workflow/ios-simulator/) 24 | - [Expo Go](https://expo.dev/go), a limited sandbox for trying out app development with Expo 25 | 26 | You can start developing by editing the files inside the **app** directory. This project uses [file-based routing](https://docs.expo.dev/router/introduction). 27 | 28 | ## Get a fresh project 29 | 30 | When you're ready, run: 31 | 32 | ```bash 33 | npm run reset-project 34 | ``` 35 | 36 | This command will move the starter code to the **app-example** directory and create a blank **app** directory where you can start developing. 37 | 38 | ## Learn more 39 | 40 | To learn more about developing your project with Expo, look at the following resources: 41 | 42 | - [Expo documentation](https://docs.expo.dev/): Learn fundamentals, or go into advanced topics with our [guides](https://docs.expo.dev/guides). 43 | - [Learn Expo tutorial](https://docs.expo.dev/tutorial/introduction/): Follow a step-by-step tutorial where you'll create a project that runs on Android, iOS, and the web. 44 | 45 | ## Join the community 46 | 47 | Join our community of developers creating universal apps. 48 | 49 | - [Expo on GitHub](https://github.com/expo/expo): View our open source platform and contribute. 50 | - [Discord community](https://chat.expo.dev): Chat with Expo users and ask questions. 51 | -------------------------------------------------------------------------------- /apps/native/app-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /apps/native/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "universal-bottom-sheet", 4 | "slug": "universal-bottom-sheet", 5 | "version": "1.0.0", 6 | "orientation": "portrait", 7 | "icon": "./assets/images/icon.png", 8 | "scheme": "universal-bottom-sheet", 9 | "userInterfaceStyle": "automatic", 10 | "splash": { 11 | "image": "./assets/images/splash.png", 12 | "resizeMode": "contain", 13 | "backgroundColor": "#ffffff" 14 | }, 15 | "ios": { 16 | "supportsTablet": true 17 | }, 18 | "android": { 19 | "adaptiveIcon": { 20 | "foregroundImage": "./assets/images/adaptive-icon.png", 21 | "backgroundColor": "#ffffff" 22 | } 23 | }, 24 | "web": { 25 | "bundler": "metro", 26 | "favicon": "./assets/images/favicon.png" 27 | }, 28 | "plugins": [ 29 | "expo-router" 30 | ], 31 | "experiments": { 32 | "typedRoutes": true 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /apps/native/app/_layout.tsx: -------------------------------------------------------------------------------- 1 | import { Slot } from "expo-router"; 2 | import { GestureHandlerRootView } from "react-native-gesture-handler"; 3 | import { BottomSheetModalProvider } from "ui/bottom-sheet"; 4 | 5 | import "../global.css"; 6 | 7 | export default function RootLayout() { 8 | return ( 9 | 10 | 11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /apps/native/app/index.tsx: -------------------------------------------------------------------------------- 1 | export { Home as default } from "app/screens/home"; 2 | -------------------------------------------------------------------------------- /apps/native/assets/fonts/SpaceMono-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adebayoileri/universal-bottom-sheet/3370bd47253f4e220934d6e7efa9f64d09020144/apps/native/assets/fonts/SpaceMono-Regular.ttf -------------------------------------------------------------------------------- /apps/native/assets/images/adaptive-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adebayoileri/universal-bottom-sheet/3370bd47253f4e220934d6e7efa9f64d09020144/apps/native/assets/images/adaptive-icon.png -------------------------------------------------------------------------------- /apps/native/assets/images/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adebayoileri/universal-bottom-sheet/3370bd47253f4e220934d6e7efa9f64d09020144/apps/native/assets/images/favicon.png -------------------------------------------------------------------------------- /apps/native/assets/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adebayoileri/universal-bottom-sheet/3370bd47253f4e220934d6e7efa9f64d09020144/apps/native/assets/images/icon.png -------------------------------------------------------------------------------- /apps/native/assets/images/partial-react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adebayoileri/universal-bottom-sheet/3370bd47253f4e220934d6e7efa9f64d09020144/apps/native/assets/images/partial-react-logo.png -------------------------------------------------------------------------------- /apps/native/assets/images/react-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adebayoileri/universal-bottom-sheet/3370bd47253f4e220934d6e7efa9f64d09020144/apps/native/assets/images/react-logo.png -------------------------------------------------------------------------------- /apps/native/assets/images/react-logo@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adebayoileri/universal-bottom-sheet/3370bd47253f4e220934d6e7efa9f64d09020144/apps/native/assets/images/react-logo@2x.png -------------------------------------------------------------------------------- /apps/native/assets/images/react-logo@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adebayoileri/universal-bottom-sheet/3370bd47253f4e220934d6e7efa9f64d09020144/apps/native/assets/images/react-logo@3x.png -------------------------------------------------------------------------------- /apps/native/assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adebayoileri/universal-bottom-sheet/3370bd47253f4e220934d6e7efa9f64d09020144/apps/native/assets/images/splash.png -------------------------------------------------------------------------------- /apps/native/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true); 3 | return { 4 | presets: [ 5 | ["babel-preset-expo", { jsxImportSource: "nativewind" }], 6 | "nativewind/babel", 7 | ], 8 | plugins: ["react-native-reanimated/plugin"] 9 | }; 10 | }; -------------------------------------------------------------------------------- /apps/native/global.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; -------------------------------------------------------------------------------- /apps/native/index.js: -------------------------------------------------------------------------------- 1 | import { registerRootComponent } from "expo"; 2 | import { ExpoRoot } from "expo-router"; 3 | 4 | // Must be exported or Fast Refresh won't update the context 5 | export function App() { 6 | const ctx = require.context("./app"); 7 | return ; 8 | } 9 | 10 | registerRootComponent(App); 11 | -------------------------------------------------------------------------------- /apps/native/metro.config.js: -------------------------------------------------------------------------------- 1 | // Learn more https://docs.expo.io/guides/customizing-metro 2 | const { getDefaultConfig } = require("expo/metro-config"); 3 | const { withNativeWind } = require('nativewind/metro'); 4 | const path = require("path"); 5 | 6 | // Find the workspace root, this can be replaced with `find-yarn-workspace-root` 7 | const workspaceRoot = path.resolve(__dirname, "../.."); 8 | const projectRoot = __dirname; 9 | 10 | const config = getDefaultConfig(projectRoot); 11 | 12 | // 1. Watch all files within the monorepo 13 | config.watchFolders = [workspaceRoot]; 14 | // 2. Let Metro know where to resolve packages, and in what order 15 | config.resolver.nodeModulesPaths = [ 16 | path.resolve(projectRoot, "node_modules"), 17 | path.resolve(workspaceRoot, "node_modules") 18 | ]; 19 | // 3. Force Metro to resolve (sub)dependencies only from the `nodeModulesPaths` 20 | config.resolver.disableHierarchicalLookup = true; 21 | 22 | module.exports = withNativeWind(config, { 23 | input: './global.css' 24 | }); 25 | -------------------------------------------------------------------------------- /apps/native/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "native", 3 | "main": "index.js", 4 | "version": "1.0.0", 5 | "scripts": { 6 | "dev": "expo start", 7 | "android": "expo start --android", 8 | "ios": "expo start --ios", 9 | "web": "expo start --web", 10 | "lint": "expo lint" 11 | }, 12 | "dependencies": { 13 | "@expo/vector-icons": "^14.0.0", 14 | "@gorhom/bottom-sheet": "^4.6.3", 15 | "@react-navigation/native": "^6.0.2", 16 | "app": "*", 17 | "expo": "~51.0.14", 18 | "expo-constants": "~16.0.2", 19 | "expo-font": "~12.0.7", 20 | "expo-linking": "~6.3.1", 21 | "expo-router": "~3.5.16", 22 | "expo-splash-screen": "~0.27.5", 23 | "expo-status-bar": "~1.12.1", 24 | "expo-system-ui": "~3.0.6", 25 | "expo-web-browser": "~13.0.3", 26 | "nativewind": "^4.0.36", 27 | "react": "18.2.0", 28 | "react-dom": "18.2.0", 29 | "react-native": "0.74.2", 30 | "react-native-gesture-handler": "~2.16.1", 31 | "react-native-reanimated": "~3.10.1", 32 | "react-native-safe-area-context": "4.10.1", 33 | "react-native-screens": "3.31.1", 34 | "react-native-web": "~0.19.10", 35 | "tailwindcss": "^3.4.4", 36 | "ui": "*" 37 | }, 38 | "devDependencies": { 39 | "@babel/core": "^7.20.0", 40 | "@types/react": "~18.2.45", 41 | "typescript": "~5.3.3" 42 | }, 43 | "private": true 44 | } 45 | -------------------------------------------------------------------------------- /apps/native/tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | "./index.js", 5 | "./app/**/*.{js,jsx,ts,tsx}", 6 | "../../packages/**/*.{js,ts,jsx,tsx}", 7 | ], 8 | theme: { 9 | extend: {}, 10 | }, 11 | plugins: [], 12 | } 13 | 14 | -------------------------------------------------------------------------------- /apps/native/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": [ 4 | "**/*.ts", 5 | "**/*.tsx", 6 | ".expo/types/**/*.ts", 7 | "expo-env.d.ts" 8 | ] 9 | } 10 | -------------------------------------------------------------------------------- /apps/web/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /apps/web/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | -------------------------------------------------------------------------------- /apps/web/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | -------------------------------------------------------------------------------- /apps/web/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adebayoileri/universal-bottom-sheet/3370bd47253f4e220934d6e7efa9f64d09020144/apps/web/app/favicon.ico -------------------------------------------------------------------------------- /apps/web/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | @layer utilities { 20 | .text-balance { 21 | text-wrap: balance; 22 | } 23 | } 24 | 25 | 26 | /** 27 | Follows the setup for react-native-web: 28 | https://necolas.github.io/react-native-web/docs/setup/#root-element 29 | Plus additional React Native scroll and text parity styles for various 30 | browsers. 31 | Force Next-generated DOM elements to fill their parent's height 32 | **/ 33 | html, body, #__next { 34 | width: 100%; 35 | /* To smooth any scrolling behavior */ 36 | -webkit-overflow-scrolling: touch; 37 | margin: 0px; 38 | padding: 0px; 39 | /* Allows content to fill the viewport and go beyond the bottom */ 40 | min-height: 100%; 41 | } 42 | 43 | #__next { 44 | flex-shrink: 0; 45 | flex-basis: auto; 46 | flex-direction: column; 47 | flex-grow: 1; 48 | display: flex; 49 | flex: 1; 50 | } 51 | 52 | html { 53 | /* Prevent text size change on orientation change https://gist.github.com/tfausak/2222823#file-ios-8-web-app-html-L138 */ 54 | -webkit-text-size-adjust: 100%; 55 | height: 100%; 56 | } 57 | 58 | body { 59 | display: flex; 60 | /* Allows you to scroll below the viewport; default value is visible */ 61 | overflow-y: auto; 62 | overscroll-behavior-y: none; 63 | text-rendering: optimizeLegibility; 64 | -webkit-font-smoothing: antialiased; 65 | -moz-osx-font-smoothing: grayscale; 66 | -ms-overflow-style: scrollbar; 67 | } -------------------------------------------------------------------------------- /apps/web/app/layout.tsx: -------------------------------------------------------------------------------- 1 | "use client"; // opting out of rsc till this is fixed: https://github.com/necolas/react-native-web/discussions/2474 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const inter = Inter({ subsets: ["latin"] }); 6 | 7 | export default function RootLayout({ 8 | children, 9 | }: Readonly<{ 10 | children: React.ReactNode; 11 | }>) { 12 | return ( 13 | 14 | {children} 15 | 16 | ); 17 | } 18 | -------------------------------------------------------------------------------- /apps/web/app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | export { Home as default } from "app/screens/home"; 4 | -------------------------------------------------------------------------------- /apps/web/next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | 3 | const { withExpo } = require("@expo/next-adapter"); 4 | 5 | module.exports = withExpo({ 6 | reactStrictMode: true, 7 | transpilePackages: [ 8 | // you need to list `react-native` because `react-native-web` is aliased to `react-native`. 9 | "react-native", 10 | "react-native-web", 11 | "ui", 12 | "nativewind", 13 | "react-native-css-interop", 14 | "react-native-reanimated" 15 | // Add other packages that need transpiling 16 | ], 17 | webpack: (config) => { 18 | config.resolve.alias = { 19 | ...(config.resolve.alias || {}), 20 | // Transform all direct `react-native` imports to `react-native-web` 21 | "react-native$": "react-native-web", 22 | "react-native/Libraries/Image/AssetRegistry": 23 | "react-native-web/dist/cjs/modules/AssetRegistry", // Fix for loading images in web builds with Expo-Image 24 | }; 25 | config.resolve.extensions = [ 26 | ".web.js", 27 | ".web.jsx", 28 | ".web.ts", 29 | ".web.tsx", 30 | ...config.resolve.extensions, 31 | ]; 32 | return config; 33 | }, 34 | }); 35 | -------------------------------------------------------------------------------- /apps/web/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "web", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@expo/next-adapter": "^6.0.0", 13 | "app": "*", 14 | "next": "14.2.4", 15 | "react": "^18", 16 | "react-dom": "^18", 17 | "react-native-web": "^0.19.12", 18 | "ui": "*", 19 | "vaul": "^0.9.1" 20 | }, 21 | "devDependencies": { 22 | "@types/node": "^20", 23 | "@types/react": "^18", 24 | "@types/react-dom": "^18", 25 | "eslint": "^8", 26 | "eslint-config-next": "14.2.4", 27 | "postcss": "^8", 28 | "prettier": "^3.3.2", 29 | "tailwindcss": "^3.4.1", 30 | "typescript": "^5" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /apps/web/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /apps/web/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apps/web/tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 8 | "../../packages/**/*.{js,ts,jsx,tsx}", 9 | ], 10 | important: "html", 11 | presets: [require("nativewind/preset")], 12 | theme: { 13 | extend: {}, 14 | }, 15 | plugins: [], 16 | }; 17 | export default config; 18 | -------------------------------------------------------------------------------- /apps/web/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "jsxImportSource": "nativewind", 5 | "lib": ["dom", "dom.iterable", "esnext"], 6 | "allowJs": true, 7 | "skipLibCheck": true, 8 | "strict": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "bundler", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | }, 23 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 24 | "exclude": ["node_modules"] 25 | } 26 | -------------------------------------------------------------------------------- /github.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/adebayoileri/universal-bottom-sheet/3370bd47253f4e220934d6e7efa9f64d09020144/github.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "universal-bottom-sheet", 3 | "private": true, 4 | "scripts": { 5 | "build": "turbo run build", 6 | "dev": "turbo run dev", 7 | "clean": "turbo run clean && rm -rf node_modules", 8 | "format": "prettier --write \"**/*.{ts,tsx,md}\"" 9 | }, 10 | "workspaces": [ 11 | "apps/*", 12 | "packages/*" 13 | ], 14 | "packageManager": "yarn@1.22.19", 15 | "devDependencies": { 16 | "@gorhom/bottom-sheet": "^4.6.3", 17 | "@types/react": "^18.3.3", 18 | "@types/react-native": "^0.73.0", 19 | "prettier": "^3.3.2", 20 | "turbo": "^2.0.4" 21 | }, 22 | "resolutions": { 23 | "@gorhom/bottom-sheet": "^4.6.3", 24 | "react": "18.2.0", 25 | "react-native": "0.74.2", 26 | "react-native-reanimated": "~3.10.1", 27 | "react-native-web": "~0.19.10", 28 | "tailwindcss": "^3.4.1" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/app/index.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /packages/app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "app", 3 | "version": "1.0.0", 4 | "main": "index.ts", 5 | "license": "MIT", 6 | "dependencies": { 7 | "react-native-reanimated": "~3.10.1" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/app/screens/home.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import React, { useCallback, useMemo, useRef } from "react"; 4 | import { Text } from "ui/text"; 5 | import { View } from "ui/view"; 6 | import { 7 | BottomSheetModal, 8 | BottomSheetView, 9 | BottomSheetTrigger, 10 | BottomSheetHandle, 11 | } from "ui/bottom-sheet"; 12 | import { Pressable, Platform } from "react-native"; 13 | import { useSharedValue } from "react-native-reanimated"; 14 | 15 | export function Home() { 16 | const [isOpen, setIsOpen] = React.useState(false); 17 | 18 | const animatedIndex = useSharedValue(0); 19 | const animatedPosition = useSharedValue(0); 20 | // ref 21 | const bottomSheetModalRef = useRef(null); 22 | 23 | // bottomSheetModalRef 24 | console.log({ bottomSheetModalRef }); 25 | 26 | // variables 27 | const snapPoints = useMemo(() => [600, "20%", "50%", "70%", "95%"], []); 28 | 29 | // callbacks 30 | const handlePresentModalPress = useCallback(() => { 31 | // bottomSheetWebRef.current?.focus(); 32 | 33 | if (isOpen) { 34 | bottomSheetModalRef.current?.dismiss(); 35 | setIsOpen(false); 36 | } else { 37 | bottomSheetModalRef.current?.present(); 38 | setIsOpen(true); 39 | } 40 | }, [isOpen]); 41 | 42 | const handleSheetChanges = useCallback((index: number) => { 43 | console.log("handleSheetChanges", index); 44 | }, []); 45 | 46 | return ( 47 | 48 | 49 | {Platform.OS !== "web" && ( // Use this condition if you want to control the modal from outside for only mobile 50 | 51 | Present Modal 52 | 53 | )} 54 | 55 | ( 62 | 67 | )} 68 | > 69 | {Platform.OS === "web" && ( 70 | <> 71 | 72 | Present Modal 73 | 74 | 75 | )} 76 | 77 | {Platform.OS === "web" && ( 78 | 83 | )} 84 | Awesome 🎉 85 | 86 | 87 | 88 | 89 | ); 90 | } 91 | -------------------------------------------------------------------------------- /packages/app/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json" 3 | } -------------------------------------------------------------------------------- /packages/ui/bottom-sheet/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment, forwardRef } from "react"; 2 | import BottomSheet, { 3 | BottomSheetView as BSView, 4 | BottomSheetModalProvider, 5 | BottomSheetModal as BSModal, 6 | BottomSheetScrollView as BSScrollView, 7 | BottomSheetHandle as BSHandle 8 | } from "@gorhom/bottom-sheet"; 9 | import type { BottomSheetModal as BSModalType } from "@gorhom/bottom-sheet"; 10 | import { cssInterop } from "nativewind"; 11 | import { BottomSheetProps, BSHandleProps } from "./types"; 12 | 13 | const BottomSheetTrigger = Fragment; 14 | 15 | type BottomSheetModal = BSModalType; 16 | 17 | const BottomSheetModal = forwardRef< 18 | BSModal, 19 | BottomSheetProps & { children: React.ReactNode; isOpen?: boolean } 20 | >(({ children, ...rest }: BottomSheetProps, ref) => { 21 | return ( 22 | 23 | {children} 24 | 25 | ); 26 | }); 27 | 28 | const BottomSheetView = cssInterop(BSView, { 29 | className: "style", 30 | }); 31 | 32 | const BottomSheetScrollView = cssInterop(BSScrollView, { 33 | className: "style", 34 | contentContainerclassName: "contentContainerStyle", 35 | }); 36 | 37 | const BottomSheetHandle: React.FC = BSHandle; 38 | 39 | export { 40 | BottomSheet, 41 | BottomSheetView, 42 | BottomSheetModalProvider, 43 | BottomSheetModal, 44 | BottomSheetScrollView, 45 | BottomSheetTrigger, 46 | BottomSheetHandle, 47 | }; 48 | -------------------------------------------------------------------------------- /packages/ui/bottom-sheet/index.web.tsx: -------------------------------------------------------------------------------- 1 | import { ComponentProps, FC, Fragment, forwardRef, useMemo } from "react"; 2 | import { Platform } from "react-native"; 3 | import { Drawer } from "vaul"; 4 | import { convertSnapPoints } from "./util"; 5 | import { BSHandleProps } from "./types"; 6 | 7 | const BottomSheet = Fragment; 8 | 9 | const BottomSheetView = forwardRef< 10 | HTMLDivElement, 11 | { 12 | children: React.ReactNode; 13 | ref?: React.Ref; 14 | className?: string; 15 | } 16 | >(({ children, className }, ref) => ( 17 | 18 | 19 | 23 | {children} 24 | 25 | 26 | )); 27 | 28 | const BottomSheetModalProvider = Fragment; 29 | 30 | type BottomSheetModal = ComponentProps; 31 | 32 | const BottomSheetModal = ({ 33 | children, 34 | isOpen, 35 | snapPoints, 36 | ...rest 37 | }: { 38 | children: React.ReactNode; 39 | isOpen?: boolean; 40 | snapPoints?: string[]; 41 | }) => { 42 | const combinedSnapPoints = useMemo(() => { 43 | // Vaul uses different snap points format 44 | return convertSnapPoints(snapPoints || []); 45 | }, [snapPoints]); 46 | 47 | return ( 48 | 49 | {children} 50 | 51 | ); 52 | }; 53 | 54 | const BottomSheetScrollView = ({ children }: { children: React.ReactNode }) => ( 55 | <>{children} 56 | ); 57 | 58 | const BottomSheetTrigger = Platform.OS === "web" ? Drawer.Trigger : Fragment; 59 | 60 | const BottomSheetHandle: FC< 61 | BSHandleProps & { 62 | className?: string; 63 | animatedIndex?: number; 64 | animatedPosition?: number; 65 | } 66 | > = ({ animatedIndex = undefined, animatedPosition = undefined, ...rest }) => { 67 | if (Platform.OS === "web") return ; 68 | return ; 69 | }; 70 | 71 | export { 72 | BottomSheet, 73 | BottomSheetTrigger, 74 | BottomSheetView, 75 | BottomSheetModalProvider, 76 | BottomSheetModal, 77 | BottomSheetScrollView, 78 | BottomSheetHandle, 79 | }; 80 | -------------------------------------------------------------------------------- /packages/ui/bottom-sheet/types.tsx: -------------------------------------------------------------------------------- 1 | //#region Gorhom Bottom Sheet 2 | 3 | import { BottomSheetProps as BSProps, BottomSheetHandleProps } from "@gorhom/bottom-sheet"; 4 | 5 | export interface BSHandleProps extends BottomSheetHandleProps { 6 | className?: string; 7 | } 8 | //#endregion 9 | 10 | //#region Vaul 11 | 12 | /* eslint-disable no-unused-vars */ 13 | interface WithFadeFromProps {} 14 | interface WithoutFadeFromProps {} 15 | 16 | interface DialogProps extends WithFadeFromProps, WithoutFadeFromProps { 17 | activeSnapPoint?: number | string | null; 18 | setActiveSnapPoint?: (snapPoint: number | string | null) => void; 19 | children?: React.ReactNode; 20 | open?: boolean; 21 | closeThreshold?: number; 22 | noBodyStyles?: boolean; 23 | onOpenChange?: (open: boolean) => void; 24 | shouldScaleBackground?: boolean; 25 | setBackgroundColorOnScale?: boolean; 26 | scrollLockTimeout?: number; 27 | fixed?: boolean; 28 | dismissible?: boolean; 29 | handleOnly?: boolean; 30 | onDrag?: ( 31 | event: React.PointerEvent, 32 | percentageDragged: number 33 | ) => void; 34 | onRelease?: ( 35 | event: React.PointerEvent, 36 | open: boolean 37 | ) => void; 38 | modal?: boolean; 39 | nested?: boolean; 40 | onClose?: () => void; 41 | // vaul property for Drawer direction 42 | direction?: "top" | "bottom" | "left" | "right"; 43 | preventScrollRestoration?: boolean; 44 | disablePreventScroll?: boolean; 45 | } 46 | //#endregion 47 | 48 | 49 | //#region Shared types 50 | export interface BottomSheetProps extends DialogProps, BSProps { 51 | children: React.ReactNode; 52 | isOpen?: boolean; 53 | snapPoints?: Pick["snapPoints"]; 54 | } 55 | //#endregion 56 | 57 | -------------------------------------------------------------------------------- /packages/ui/bottom-sheet/util.ts: -------------------------------------------------------------------------------- 1 | const isPercentage = (value: string | number) => 2 | value && typeof value === "string" && value.includes("%"); 3 | 4 | const toDecimal = (value: string) => Number(value.replace("%", "")) / 100; 5 | 6 | const convertSnapPoints = (snapPoints: string[]) => 7 | snapPoints.map((point) => 8 | isPercentage(point) ? toDecimal(point) : `${point}px` 9 | ); 10 | 11 | export { convertSnapPoints }; 12 | -------------------------------------------------------------------------------- /packages/ui/index.ts: -------------------------------------------------------------------------------- 1 | // leave this blank 2 | // https://github.com/vercel/next.js/issues/12557 3 | export {}; 4 | -------------------------------------------------------------------------------- /packages/ui/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ui", 3 | "private": true, 4 | "version": "1.0.0", 5 | "main": "index.ts", 6 | "license": "MIT" 7 | } 8 | -------------------------------------------------------------------------------- /packages/ui/text/index.tsx: -------------------------------------------------------------------------------- 1 | import { Text as ReactNativeText } from "react-native"; 2 | import { cssInterop } from "nativewind"; 3 | 4 | export const Text = cssInterop(ReactNativeText, { 5 | className: "style", 6 | }); 7 | -------------------------------------------------------------------------------- /packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "include": ["."], 4 | "exclude": ["dist", "build", "node_modules"], 5 | "compilerOptions": { 6 | "strict": true 7 | } 8 | } -------------------------------------------------------------------------------- /packages/ui/view/index.tsx: -------------------------------------------------------------------------------- 1 | import { View as ReactNativeView } from "react-native"; 2 | import { cssInterop } from "nativewind"; 3 | 4 | export const View = cssInterop(ReactNativeView, { 5 | className: "style", 6 | }); 7 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | // Add file for tailwind intellisense 2 | module.exports = {}; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strictNullChecks": true, 4 | "noUncheckedIndexedAccess": true, 5 | "baseUrl": "./packages", 6 | "paths": { 7 | "ui/*": ["./ui/*"], 8 | "app/*": ["./app/*"] 9 | }, 10 | "jsx": "react-jsx" 11 | }, 12 | "exclude": ["node_modules", "packages/*/node_modules", "packages/*/dist"], 13 | "extends": "expo/tsconfig.base" 14 | } -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "tasks": { 4 | "build": { 5 | "outputs": [".next/**", "!.next/cache/**"], 6 | "dependsOn": ["^build"] 7 | }, 8 | "dev": { 9 | "cache": false, 10 | "persistent": true 11 | }, 12 | "lint": {}, 13 | "clean": { 14 | "cache": false 15 | } 16 | } 17 | } --------------------------------------------------------------------------------