react-modal-sheet 4.3.0 → 5.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align='center'>
2
- <img src="media/banner.jpg" alt="React Modal Sheet logo"/>
2
+ <img src="media/hero.jpg" alt="React Modal Sheet logo"/>
3
3
  <p/>
4
4
 
5
5
  <div align="center" >
@@ -10,12 +10,13 @@
10
10
  <br/>
11
11
  <img alt="npm version" src="https://img.shields.io/npm/v/react-modal-sheet?style=for-the-badge">
12
12
  <img alt="npm license" src="https://img.shields.io/npm/l/react-modal-sheet?style=for-the-badge">
13
+ <img alt="npm downloads" src="https://img.shields.io/npm/dm/react-modal-sheet?style=for-the-badge">
13
14
  <br/>
14
15
  <br/>
15
16
  </div>
16
17
 
17
- | ![](media/1.gif) | ![](media/2.gif) | ![](media/3.gif) | ![](media/4.gif) |
18
- | :--------------: | :--------------: | :--------------: | :--------------: |
18
+ | ![](media/example-apple-maps.gif) | ![](media/example-apple-music.gif) | ![](media/example-snap-points.gif) | ![](media/example-scrollable.gif) |
19
+ | :-------------------------------: | :--------------------------------: | :--------------------------------: | :-------------------------------: |
19
20
 
20
21
  ## 📦 Installation
21
22
 
@@ -31,14 +32,12 @@ The gestures and animations are handled by the excellent [Motion](https://motion
31
32
  npm install motion
32
33
  ```
33
34
 
34
- > [!IMPORTANT]
35
- > If you are still using the old `framer-motion` package you need to upgrade to `motion` as `react-modal-sheet` is now built on top of the new `motion` package. **Version `v3.5.0` and older are compatible with `framer-motion`**.
36
-
37
35
  ---
38
36
 
39
37
  <details>
40
38
  <summary><strong>📚 Table of contents</strong></summary>
41
39
 
40
+ - [What's new in v5](#whats-new-in-v5)
42
41
  - [Usage](#-usage)
43
42
  - [Props](#%EF%B8%8F-props)
44
43
  - [Methods and properties](#%EF%B8%8F-methods-and-properties)
@@ -51,6 +50,37 @@ npm install motion
51
50
 
52
51
  ---
53
52
 
53
+ ## 🎉 What's new in v5
54
+
55
+ Version 5 introduces several major improvements and breaking changes:
56
+
57
+ ### 🔄 Breaking Changes
58
+
59
+ - **Removed `Sheet.Scroller`**: Scrolling is now handled automatically by `Sheet.Content`
60
+ - **Snap point order reversed**: Snap points now use ascending order (e.g., `[0, 0.5, 1]` instead of `[1, 0.5, 0]`)
61
+ - This aligns better with other bottom sheet libraries and makes more intuitive sense
62
+ - **Detent prop values changed**:
63
+ - `"full-height"` → `"default"`
64
+ - `"content-height"` → `"content"`
65
+ - New `"full"` detent for viewport-filling sheets
66
+
67
+ ### ✨ New Features
68
+
69
+ - **Built-in keyboard avoidance**: Best-effort automatic virtual keyboard handling with the `avoidKeyboard` prop
70
+ - **Enhanced scroll control**: Dynamic `disableScroll` and `disableDrag` functions with scroll state
71
+ - **Improved snap point handling**: Better snap point calculation and more natural snapping between points
72
+ - **New sheet properties**: Access `height` and `yInverted` motion values via ref
73
+ - **Prevent dismissal**: New `disableDismiss` prop to prevent sheet from being closed by user gestures
74
+
75
+ ### 🔧 Migration from v4
76
+
77
+ 1. Remove all `Sheet.Scroller` components - content is now scrollable by default
78
+ 2. Reverse your snap point arrays: `[1, 0.5, 0]` → `[0, 0.5, 1]`
79
+ 3. Update detent props: `detent="full-height"` → `detent="default"`
80
+ 4. Review virtual keyboard handling - it's now automatic with `avoidKeyboard={true}`
81
+
82
+ ---
83
+
54
84
  ## 💻 Usage
55
85
 
56
86
  ```tsx
@@ -78,34 +108,36 @@ function Example() {
78
108
 
79
109
  The `Sheet` component follows the [Compound Component pattern](https://kentcdodds.com/blog/compound-components-with-react-hooks) in order to provide a flexible yet powerful API for creating highly customizable bottom sheet components.
80
110
 
81
- Since the final bottom sheet is composed from smaller building blocks (`Container`, `Content`, `Scroller`, `Header`, and `Backdrop`) you are in total control over the rendering output. So for example, if you don't want to have any backdrop in your sheet then you can just skip rendering it instead of passing some prop like `renderBackdrop={false}` to the main sheet component. Cool huh? 😎
111
+ Since the final bottom sheet is composed from smaller building blocks (`Container`, `Content`, `Header`, and `Backdrop`) you are in total control over the rendering output. So for example, if you don't want to have any backdrop in your sheet then you can just skip rendering it instead of passing some prop like `renderBackdrop={false}` to the main sheet component. Cool huh? 😎
82
112
 
83
- Also, by constructing the sheet from smaller pieces makes it easier to apply any necessary accessibility related properties to the right components without requiring the main sheet component to be aware of them. You can read more about accessibility in the [Accessibility](#Accessibility) section.
113
+ Also, by constructing the sheet from smaller pieces makes it easier to apply any necessary accessibility related properties to the right components without requiring the main sheet component to be aware of them. You can read more about accessibility in the Accessibility section below.
84
114
 
85
115
  ## 🎛️ Props
86
116
 
87
- | Name | Required | Default | Description |
88
- | ----------------------- | -------- | ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
89
- | `children` | yes | | Use `Sheet.Container/Content/Header/Backdrop` to compose your bottom sheet. |
90
- | `isOpen` | yes | | Boolean that indicates whether the sheet is open or not. |
91
- | `onClose` | yes | | Callback fn that is called when the sheet is closed by the user. |
92
- | `disableDrag` | no | false | Disable drag for the whole sheet. |
93
- | `disableScrollLocking` | no | false | Disable scroll locking for the `body` element while sheet is open. Can be useful if you face issues with input elements and the iOS virtual keyboard. See related [issue](https://github.com/Temzasse/react-modal-sheet/issues/135). |
94
- | `detent` | no | `'full-height'` | The [detent](https://developer.apple.com/design/human-interface-guidelines/components/presentation/sheets#ios-ipados) in which the sheet should be in when opened. Available values: `'full-height'` or `'content-height'`. |
95
- | `onOpenStart` | no | | Callback fn that is called when the sheet opening animation starts. |
96
- | `onOpenEnd` | no | | Callback fn that is called when the sheet opening animation is completed. |
97
- | `onCloseStart` | no | | Callback fn that is called when the sheet closing animation starts. |
98
- | `onCloseEnd` | no | | Callback fn that is called when the sheet closing animation is completed. |
99
- | `onSnap` | no | | Callback fn that is called with the current snap point index when the sheet snaps to a new snap point. Requires `snapPoints` prop. |
100
- | `snapPoints` | no | | Eg. `[-50, 0.5, 100, 0]` - where positive values are pixels from the bottom of the sheet and negative from the top. Values between 0-1 represent percentages, eg. `0.5` means 50% of sheet height. **Prefer using `1` for representing a snap point for a fully visible sheet.** |
101
- | `initialSnap` | no | 0 | Initial snap point when sheet is opened (index from `snapPoints`). |
102
- | `modalEffectRootId` | no | | The id of the element where the main app is mounted, eg. "root". Enables [iOS modal effect](#-ios-modal-view-effect). |
103
- | `modalEffectThreshold` | no | 0 | Threshold value between 0-1 which determines when the iOS modal effect will start while dragging the sheet - `0` corresponding to the start of the drag (0% has been dragged into view) and `1` corresponding to the end of the drag (100% of the sheet is visible). |
104
- | `tweenConfig` | no | `{ ease: 'easeOut', duration: 0.2 }` | Overrides the config for the sheet [tween](https://motion.dev/docs/react-transitions#tween) transition when the sheet is opened, closed, or snapped to a point. |
105
- | `mountPoint` | no | `document.body` | HTML element that should be used as the mount point for the sheet. |
106
- | `prefersReducedMotion` | no | false | Skip sheet animations (sheet instantly snaps to desired location). |
107
- | `dragVelocityThreshold` | no | 500 | How fast the sheet must be flicked down to close. Higher values make the sheet harder to close. |
108
- | `dragCloseThreshold` | no | 0.6 | The portion of the sheet which must be dragged off-screen before it will close. |
117
+ | Name | Required | Default | Description |
118
+ | ----------------------- | -------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
119
+ | `children` | yes | | Use `Sheet.Container/Content/Header/Backdrop` to compose your bottom sheet. |
120
+ | `isOpen` | yes | | Boolean that indicates whether the sheet is open or not. |
121
+ | `onClose` | yes | | Callback fn that is called when the sheet is closed by the user. |
122
+ | `avoidKeyboard` | no | true | Automatically avoid the virtual keyboard by adding bottom padding when the keyboard is open. Only works on mobile devices with [Virtual Keyboard](https://developer.mozilla.org/en-US/docs/Web/API/VirtualKeyboard_API) or [Visual Viewport](https://developer.mozilla.org/en-US/docs/Web/API/Visual_Viewport_API) API support. |
123
+ | `disableDrag` | no | false | Disable drag for the whole sheet. |
124
+ | `disableDismiss` | no | false | Disable dismissing the sheet via dragging or high velocity swipe. When enabled, the sheet can only be closed programmatically. |
125
+ | `disableScrollLocking` | no | false | Disable scroll locking for the `body` element while sheet is open. Can be useful if you face issues with input elements and the iOS virtual keyboard. See related [issue](https://github.com/Temzasse/react-modal-sheet/issues/135). |
126
+ | `detent` | no | `'default'` | The [detent](https://developer.apple.com/design/human-interface-guidelines/components/presentation/sheets#ios-ipados) in which the sheet should be in when opened. Available values: `'default'`, `'content'`, or `'full'`. |
127
+ | `onOpenStart` | no | | Callback fn that is called when the sheet opening animation starts. |
128
+ | `onOpenEnd` | no | | Callback fn that is called when the sheet opening animation is completed. |
129
+ | `onCloseStart` | no | | Callback fn that is called when the sheet closing animation starts. |
130
+ | `onCloseEnd` | no | | Callback fn that is called when the sheet closing animation is completed. |
131
+ | `onSnap` | no | | Callback fn that is called with the current snap point index when the sheet snaps to a new snap point. Requires `snapPoints` prop. |
132
+ | `snapPoints` | no | | Eg. `[0, 0.5, 100, 1]` - where positive values are pixels from the bottom of the sheet and negative from the top. Values between 0-1 represent percentages, eg. `0.5` means 50% of sheet height. **Must be in ascending order and should include 0 (closed) and 1 (fully open).** |
133
+ | `initialSnap` | no | 0 | Initial snap point when sheet is opened (index from `snapPoints`). |
134
+ | `modalEffectRootId` | no | | The id of the element where the main app is mounted, eg. "root". Enables [iOS modal effect](#-ios-modal-view-effect). |
135
+ | `modalEffectThreshold` | no | 0 | Threshold value between 0-1 which determines when the iOS modal effect will start while dragging the sheet - `0` corresponding to the start of the drag (0% has been dragged into view) and `1` corresponding to the end of the drag (100% of the sheet is visible). |
136
+ | `tweenConfig` | no | `{ ease: 'easeOut', duration: 0.2 }` | Overrides the config for the sheet [tween](https://motion.dev/docs/react-transitions#tween) transition when the sheet is opened, closed, or snapped to a point. |
137
+ | `mountPoint` | no | `document.body` | HTML element that should be used as the mount point for the sheet. |
138
+ | `prefersReducedMotion` | no | false | Skip sheet animations (sheet instantly snaps to desired location). |
139
+ | `dragVelocityThreshold` | no | 500 | How fast the sheet must be flicked down to close. Higher values make the sheet harder to close. |
140
+ | `dragCloseThreshold` | no | 0.6 | The portion of the sheet which must be dragged off-screen before it will close. |
109
141
 
110
142
  ## ⚙️ Methods and properties
111
143
 
@@ -117,9 +149,9 @@ Imperative method that can be accessed via a ref for snapping to a snap point in
117
149
  import { Sheet, SheetRef } from 'react-modal-sheet';
118
150
  import { useState, useRef } from 'react';
119
151
 
120
- const snapPoints = [1, 0.5, 0];
152
+ const snapPoints = [0, 0.5, 1];
121
153
 
122
- function Example() {
154
+ function SnapExample() {
123
155
  const [isOpen, setOpen] = useState(false);
124
156
  const ref = useRef<SheetRef>(null);
125
157
  const snapTo = (i: number) => ref.current?.snapTo(i);
@@ -144,7 +176,6 @@ function Example() {
144
176
  <button onClick={() => snapTo(0)}>Snap to index 0</button>
145
177
  <button onClick={() => snapTo(1)}>Snap to index 1</button>
146
178
  <button onClick={() => snapTo(2)}>Snap to index 2</button>
147
- <button onClick={() => snapTo(3)}>Snap to index 3</button>
148
179
  </Sheet.Content>
149
180
  </Sheet.Container>
150
181
  </Sheet>
@@ -153,47 +184,43 @@ function Example() {
153
184
  }
154
185
  ```
155
186
 
156
- ### Motion value `y`
187
+ ### Motion values `y`, `yInverted` and `height`
188
+
189
+ The `y` value is an internal [MotionValue](https://motion.dev/docs/react-motion-value) that represents the distance to the top most position of the sheet when it is fully open. So for example the `y` value is zero when the sheet is completely open.
157
190
 
158
- The `y` value is an internal `MotionValue` that represents the distance to the top most position of the sheet when it is fully open. So for example the `y` value is zero when the sheet is completely open.
191
+ The `yInverted` value is the inverse of the `y` value and represents the distance from the bottom of the sheet. This can be useful for certain animations or calculations.
159
192
 
160
- Similarly to the `snapTo` method the `y` value can be accessed via a ref.
193
+ The `height` value represents the current height of the sheet in pixels.
161
194
 
162
- The `y` value can be useful for certain situtation eg. when you want to combine snap points with scrollable sheet content and ensure that the content stays properly scrollable in any snap point. Below you can see a simplified example of this situation and for a more detailed example take a look at the [ScrollableSnapPoints](example/components/ScrollableSnapPoints.tsx) component in the example app.
195
+ All these values can be accessed via a ref, similar to the `snapTo` method.
196
+
197
+ Below you can see an example of how to use these values:
163
198
 
164
199
  ```tsx
165
200
  import { Sheet, SheetRef } from 'react-modal-sheet';
166
201
  import { useState, useRef } from 'react';
167
202
 
168
- const snapPoints = [1, 0.5, 0];
169
-
170
- function Example() {
203
+ function RefExample() {
171
204
  const [isOpen, setOpen] = useState(false);
172
205
  const ref = useRef<SheetRef>(null);
173
206
 
207
+ function doSomething() {
208
+ console.log('> Current y value:', ref.current?.y.get());
209
+ console.log('> Current yInverted value:', ref.current?.yInverted.get());
210
+ console.log('> Current height value:', ref.current?.height);
211
+ }
212
+
174
213
  return (
175
214
  <>
176
215
  <button onClick={() => setOpen(true)}>Open sheet</button>
177
216
 
178
- <Sheet
179
- isOpen={isOpen}
180
- onClose={() => setOpen(false)}
181
- initialSnap={1}
182
- snapPoints={snapPoints}
183
- >
217
+ <Sheet ref={ref} isOpen={isOpen} onClose={() => setOpen(false)}>
184
218
  <Sheet.Container>
185
- {/**
186
- * Since `Sheet.Content` is a `motion.div` it can receive motion values
187
- * in it's style prop which allows us to utilise the exposed `y` value.
188
- *
189
- * By syncing the padding bottom with the `y` motion value we introduce
190
- * an offset that ensures that the sheet content can be scrolled all the
191
- * way to the bottom in every snap point.
192
- */}
193
- <Sheet.Content style={{ paddingBottom: ref.current?.y }}>
194
- <Sheet.Scroller draggableAt="both">
195
- {/* Some content here that makes the sheet content scrollable */}
196
- </Sheet.Scroller>
219
+ <Sheet.Content>
220
+ <motion.div style={{ paddingBottom: ref.current?.y }}>
221
+ Use animated y value in some way
222
+ </motion.div>
223
+ {/* Your content here */}
197
224
  </Sheet.Content>
198
225
  </Sheet.Container>
199
226
  </Sheet>
@@ -204,23 +231,19 @@ function Example() {
204
231
 
205
232
  ### Detents
206
233
 
207
- By default the sheet will take the full height of the page minus top padding and safe area inset. If you want the sheet height to be based on it's content you can pass `detent="content-height"` prop to the `Sheet` component:
234
+ By default the sheet will take the full height of the page minus top padding and safe area inset. The `detent` prop controls the sheet's height behavior:
208
235
 
209
- ```tsx
210
- function Example() {
211
- const [isOpen, setOpen] = useState(false);
236
+ - `"default"` - Sheet takes full height minus safe areas (default behavior)
237
+ - `"content"` - Sheet height is based on its content
238
+ - `"full"` - Sheet takes the entire viewport height with no safe area insets
212
239
 
240
+ ```tsx
241
+ function DetentExample() {
213
242
  return (
214
- <Sheet
215
- isOpen={isOpen}
216
- onClose={() => setOpen(false)}
217
- detent="content-height"
218
- >
243
+ <Sheet detent="content">
219
244
  <Sheet.Container>
220
245
  <Sheet.Content>
221
- <Sheet.Scroller>
222
- <div style={{ height: 200 }}>Some content</div>
223
- </Sheet.Scroller>
246
+ <div style={{ height: 200 }}>Some content</div>
224
247
  </Sheet.Content>
225
248
  </Sheet.Container>
226
249
  </Sheet>
@@ -228,9 +251,9 @@ function Example() {
228
251
  }
229
252
  ```
230
253
 
231
- If the sheet height changes dynamically the sheet will grow until it hits the maximum full height after which it becomes scrollable.
254
+ When using `detent="content"` and the sheet height changes dynamically, the sheet will grow until it hits the maximum default height, after which it becomes scrollable.
232
255
 
233
- It is possible to use snap points with `detent="content-height"` **but** the snap points are restricted by the content height. For example if one of the snap points is 800px and the sheet height is only 700px then snapping to the 800px snap point would only snap to 700px since otherwise the sheet would become detached from the bottom.
256
+ It is possible to use snap points with `detent="content"` **but** the snap points are restricted by the content height. For example if one of the snap points is 800px and the sheet height is only 700px then snapping to the 800px snap point would only snap to 700px since otherwise the sheet would become detached from the bottom.
234
257
 
235
258
  > ℹ️ If you are wondering where the term `detent` comes from it's from Apple's [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/components/presentation/sheets#ios-ipados).
236
259
 
@@ -262,28 +285,17 @@ Sheet header acts as a drag target and has a dragging direction indicator. Rende
262
285
 
263
286
  ### `Sheet.Content`
264
287
 
265
- Sheet content acts as a drag target and can be used in conjunction with `Sheet.Scroller` to make sure that content which doesn't fit inside the sheet becomes scrollable.
288
+ Sheet content acts as a drag target and handles scrollable content internally. It automatically manages scroll behavior and keyboard avoidance.
266
289
 
267
- > 🖥 Rendered element: `motion.div`.
290
+ > 🖥 Rendered element: `motion.div` (with internal scroller).
268
291
 
269
292
  #### Content props
270
293
 
271
- | Name | Required | Default | Description |
272
- | ------------- | -------- | ------- | ----------------------------------- |
273
- | `disableDrag` | no | false | Disable drag for the sheet content. |
274
-
275
- ### `Sheet.Scroller`
276
-
277
- Sheet scroller can be used to make the whole sheet content or parts of it scrollable in a way that drag gestures are properly disabled and enabled based on the scroll state. See the [Scrolling on touch devices](#scrolling-on-touch-devices) section for more details.
278
-
279
- > 🖥 Rendered element: `motion.div`.
280
-
281
- #### Scroller props
282
-
283
- | Name | Required | Default | Description |
284
- | --------------- | -------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
285
- | `draggableAt` | no | `"top"` | Should the drag be enabled when the element is scrolled either to the top, bottom, or both. Available values: `top`, `bottom`, `both`. |
286
- | `disableScroll` | no | false | Disable scrolling. This can be combined with snap points to make the sheet scroller only scrollable at a given snap point (usually the top position). |
294
+ | Name | Required | Default | Description |
295
+ | --------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
296
+ | `disableDrag` | no | false | Disable drag for the sheet content. Can be a boolean or a function that receives `{ scrollPosition, currentSnap }` and returns a boolean for dynamic control. |
297
+ | `disableScroll` | no | false | Disable scrolling. Can be a boolean or a function that receives `{ scrollPosition, currentSnap }` and returns a boolean. Useful for making content only scrollable at specific snap points. |
298
+ | `scrollRef` | no | | Optional ref to the internal scroll container for accessing scroll methods. |
287
299
 
288
300
  ### `Sheet.Backdrop`
289
301
 
@@ -293,37 +305,48 @@ Sheet backdrop is a translucent overlay that helps to separate the sheet from it
293
305
 
294
306
  > 🖥 Rendered element: `motion.div` or `motion.button`.
295
307
 
296
- ## ✨ Advanced usage
308
+ ## ✨ Advanced behaviors
297
309
 
298
310
  ### ⌨️ Virtual keyboard avoidance
299
311
 
300
- A common problem with React Modal Sheet is that since the sheet is rendered as a fixed positioned element it doesn't move when the virtual keyboard is opened which can lead to the keyboard covering the input elements.
301
-
302
- Handling the virtual keyboard on mobile devices can be quite tricky. It doesn't help that the Web doesn't yet have widely supported APIs for working with the virtual keyboard.
303
-
304
- **⚠️ Note: This library does not provide any official built-in solution for keyboard avoidance!**
305
-
306
- This is because it is not possible for the library to know how your sheet content is structured and how the sheet should behave when the keyboard is opened.
312
+ React Modal Sheet v5 includes built-in virtual keyboard avoidance that works automatically on mobile devices.
313
+ When the `avoidKeyboard` prop is enabled (which is the default), the sheet will automatically add bottom padding to avoid the virtual keyboard covering input elements.
307
314
 
308
- However, there are some strategies that you can use to handle the virtual keyboard:
309
-
310
- 1. Design your sheet content in a way that the input elements are positioned at the top of the sheet so that they are not covered by the keyboard. This is the easiest solution and if you look at popular mobile apps you can see that they often have input elements at the top of the screen exactly for this reason.
315
+ ```tsx
316
+ function KeyboardExample() {
317
+ return (
318
+ <Sheet avoidKeyboard>
319
+ <Sheet.Container>
320
+ <Sheet.Header />
321
+ <Sheet.Content>
322
+ <input placeholder="Your input here" />
323
+ <textarea placeholder="Your message here" />
324
+ {/* More form elements */}
325
+ </Sheet.Content>
326
+ </Sheet.Container>
327
+ <Sheet.Backdrop />
328
+ </Sheet>
329
+ );
330
+ }
331
+ ```
311
332
 
312
- 2. Listen to changes in the `visualViewport`, see [Visual Viewport API](https://developer.mozilla.org/en-US/docs/Web/API/Visual_Viewport_API), and change styles like padding on the sheet content to move the input up when the keyboard is opened. See the `AvoidKeyboard` component [code](example/components/AvoidKeyboard.tsx) and [demo](https://temzasse.github.io/react-modal-sheet/#/avoid-keyboard) in the example app for a simple example of this. The example is based on a custom `useAnimatedVirtualKeyboard` hook which you can copy to your project at your own risk. **Note that at least on iOS there is a noticeable delay when the keyboard is opened and when the `resize` event is fired for the `visualViewport`**.
333
+ The keyboard avoidance feature:
313
334
 
314
- 3. Use the experimental [VirtualKeyboard API](https://developer.mozilla.org/en-US/docs/Web/API/VirtualKeyboard_API) to detect when the virtual keyboard is opened and adjust the sheet content accordingly. **Note that this API is not yet widely supported in browsers, but it will most likely produce the best result.**
335
+ - Uses the `env(keyboard-inset-height)` from [Virtual Keyboard API](https://developer.mozilla.org/en-US/docs/Web/API/VirtualKeyboard_API) and fallbacks to [Visual Viewport API](https://developer.mozilla.org/en-US/docs/Web/API/Visual_Viewport_API) and custom `keyboard-inset-height` CSS environment variable
336
+ - Works automatically on mobile devices with virtual keyboard support
337
+ - Applies dynamic padding to keep content visible when the keyboard opens
338
+ - Disables drag gestures while the keyboard is open to prevent glitches
315
339
 
316
- ### 📱 Scrolling on touch devices
340
+ If you need to disable keyboard avoidance, set `avoidKeyboard={false}`.
317
341
 
318
- Scrolling and dragging are **the same** gesture on touch devices which can create problems when you want to have scrollable content inside the sheet. React Modal Sheet provides a `Sheet.Scroller` component that is able to automatically disable and enable dragging inside the `Sheet.Content` component based on the scroll state. There are three modes for the `Sheet.Scroller` reflected in the `draggableAt` prop:
342
+ > [!NOTE]
343
+ > If your sheet content is very long and some inputs are below the keyboard even after adding the padding, you might need to scroll the content into view manually when an input is focused.
319
344
 
320
- 1. Enable dragging when the scroller is not yet scrolled - it is at the `top` position.
321
- 2. Enable dragging when the scroller is scrolled all the way to the bottom - it is at the `bottom` position.
322
- 3. Enable dragging in `both` positions.
345
+ ### 📱 Scrolling behavior
323
346
 
324
- The scroller component is in-between these states when the user has scrolled only some amount. Dragging is always **disabled** in this in-between state in order to avoid it getting mixed with dragging gestures. The `Sheet.Scroller` component applies the special handling only for touch devices.
347
+ The `Sheet.Content` component manages scroll interactions internally and provides several ways to control scrolling:
325
348
 
326
- The default value for the `draggableAt` prop is `top` which should be a good default for most use cases. You shouldn't need `bottom` or `both` unless you have scrollable content inside a sheet that also has snap points.
349
+ #### Basic scrollable content
327
350
 
328
351
  ```tsx
329
352
  function ScrollableExample() {
@@ -332,7 +355,8 @@ function ScrollableExample() {
332
355
  <Sheet.Container>
333
356
  <Sheet.Header />
334
357
  <Sheet.Content>
335
- <Sheet.Scroller>{/*...*/}</Sheet.Scroller>
358
+ {/* Long content that needs scrolling */}
359
+ <div style={{ height: '200vh' }}>Long content here...</div>
336
360
  </Sheet.Content>
337
361
  </Sheet.Container>
338
362
  <Sheet.Backdrop />
@@ -341,31 +365,46 @@ function ScrollableExample() {
341
365
  }
342
366
  ```
343
367
 
344
- #### Disabling scroll manually
345
-
346
- You can also disable scrolling manually by passing `disableScroll` prop to the `Sheet.Scroller` component. This can be useful if you want to have a scrollable sheet but only at a certain snap point.
368
+ #### Controlling scroll behavior with snap points
347
369
 
348
- For example, if you have a snap point at the top of the sheet and you want to make the content scrollable only when the sheet is at that snap point, you can do the following:
370
+ You can disable scrolling conditionally using the `disableScroll` prop. This is useful when you want content to be scrollable only at certain snap points:
349
371
 
350
372
  ```tsx
351
- const snapPoints = [1, 0.5, 0];
352
- const initialSnap = 1;
373
+ const snapPoints = [0, 0.5, 1];
353
374
 
354
- function DisableScrollExample() {
355
- const [currentSnap, setCurrentSnap] = useState(initialSnap);
375
+ function ConditionalScrollExample() {
376
+ return (
377
+ <Sheet snapPoints={snapPoints} initialSnap={1}>
378
+ <Sheet.Container>
379
+ <Sheet.Header />
380
+ <Sheet.Content
381
+ // Only allow scrolling when at the top snap point (index 2)
382
+ disableScroll={(state) => state.currentSnap !== 2}
383
+ >
384
+ <div style={{ height: '200vh' }}>Long content here...</div>
385
+ </Sheet.Content>
386
+ </Sheet.Container>
387
+ <Sheet.Backdrop />
388
+ </Sheet>
389
+ );
390
+ }
391
+ ```
392
+
393
+ #### Dynamic drag control
356
394
 
395
+ Similarly, you can control when dragging is enabled based on scroll position or snap point:
396
+
397
+ ```tsx
398
+ function DynamicDragExample() {
357
399
  return (
358
- <Sheet
359
- initialSnap={initialSnap}
360
- snapPoints={snapPoints}
361
- onSnap={setCurrentSnap}
362
- >
400
+ <Sheet>
363
401
  <Sheet.Container>
364
402
  <Sheet.Header />
365
- <Sheet.Content>
366
- <Sheet.Scroller draggableAt="top" disableScroll={currentSnap !== 0}>
367
- {/*...*/}
368
- </Sheet.Scroller>
403
+ <Sheet.Content
404
+ // Disable drag when not scrolled to top
405
+ disableDrag={(state) => state.scrollPosition !== 'top'}
406
+ >
407
+ <div style={{ height: '200vh' }}>Long content here...</div>
369
408
  </Sheet.Content>
370
409
  </Sheet.Container>
371
410
  <Sheet.Backdrop />
@@ -374,12 +413,17 @@ function DisableScrollExample() {
374
413
  }
375
414
  ```
376
415
 
416
+ The scroll state provides:
417
+
418
+ - `scrollPosition`: `'top'`, `'bottom'`, `'middle'`, or `undefined`
419
+ - `currentSnap`: Current snap point index (if using snap points)
420
+
377
421
  ### 🪟 iOS Modal View effect
378
422
 
379
423
  In addition to the `Sheet.Backdrop` it's possible to apply a scaling effect to the main app element to highlight the modality of the bottom sheet. This effect mimics the [iOS Modal View](https://developer.apple.com/design/human-interface-guidelines/ios/app-architecture/modality/) presentation style to bring more focus to the sheet and add some delight to the user experience.
380
424
 
381
- | ![](media/3.gif) | ![](media/4.gif) |
382
- | :--------------: | :--------------: |
425
+ | ![iOS Modal Effect Demo 1](media/example-apple-music.gif) | ![iOS Modal Effect Demo 2](media/example-snap-points.gif) |
426
+ | :-------------------------------------------------------: | :-------------------------------------------------------: |
383
427
 
384
428
  To enable this effect you can provide the id of the root element where your application is mounted:
385
429
 
@@ -434,8 +478,45 @@ function Example() {
434
478
 
435
479
  You can add your own styles or override the default sheet styles via the exposed class names. Note that you might need to use `!important` for style overrides since the inner styles are applied as inline styles which have higher specificity.
436
480
 
481
+ ### CSS Modules
482
+
483
+ ```tsx
484
+ import styles from './styles.css';
485
+
486
+ function Example() {
487
+ return (
488
+ <Sheet>
489
+ <Sheet.Container className={styles.sheetContainer}>
490
+ <Sheet.Header className={styles.sheetHeader} />
491
+ <Sheet.Content className={styles.sheetContent}>{/*...*/}</Sheet.Content>
492
+ </Sheet.Container>
493
+ <Sheet.Backdrop className={styles.sheetBackdrop} />
494
+ </Sheet>
495
+ );
496
+ }
497
+ ```
498
+
499
+ ```css
500
+ /* styles.css */
501
+
502
+ .sheetContainer {
503
+ /* custom styles */
504
+ }
505
+ .sheetHeader {
506
+ /* custom styles */
507
+ }
508
+ .sheetContent {
509
+ /* custom styles */
510
+ }
511
+ .sheetBackdrop {
512
+ /* custom styles */
513
+ }
514
+ ```
515
+
437
516
  #### Vanilla CSS
438
517
 
518
+ You can also use vanilla CSS to override the styles by targeting the exposed class names:
519
+
439
520
  ```css
440
521
  .react-modal-sheet-backdrop {
441
522
  /* custom styles */
@@ -457,47 +538,52 @@ You can add your own styles or override the default sheet styles via the exposed
457
538
  #### CSS-in-JS
458
539
 
459
540
  ```tsx
460
- import { Sheet } from 'react-modal-sheet';
461
- import { styled } from 'styled-components';
462
- import { useState } from 'react';
463
-
464
- const CustomSheet = styled(Sheet)`
465
- .react-modal-sheet-backdrop {
466
- /* custom styles */
467
- }
468
- .react-modal-sheet-container {
469
- /* custom styles */
470
- }
471
- .react-modal-sheet-header {
472
- /* custom styles */
473
- }
474
- .react-modal-sheet-drag-indicator {
475
- /* custom styles */
476
- }
477
- .react-modal-sheet-content {
478
- /* custom styles */
479
- }
480
- `;
541
+ import { styled } from 'styled-components'; // or emotion, pandacss, etc.
481
542
 
482
543
  function Example() {
483
- const [isOpen, setOpen] = useState(false);
484
-
485
544
  return (
486
- <>
487
- <button onClick={() => setOpen(true)}>Open sheet</button>
488
-
489
- <CustomSheet isOpen={isOpen} onClose={() => setOpen(false)}>
490
- <Sheet.Container>
491
- <Sheet.Header />
492
- <Sheet.Content>{/*...*/}</Sheet.Content>
493
- </Sheet.Container>
494
- <Sheet.Backdrop />
495
- </CustomSheet>
496
- </>
545
+ <Sheet>
546
+ <SheetContainer>
547
+ <SheetHeader />
548
+ <SheetContent>{/*...*/}</SheetContent>
549
+ </SheetContainer>
550
+ <SheetBackdrop />
551
+ </Sheet>
497
552
  );
498
553
  }
554
+
555
+ const SheetContainer = styled(Sheet.Container)`
556
+ /* custom styles */
557
+ `;
558
+
559
+ const SheetHeader = styled(Sheet.Header)`
560
+ /* custom styles */
561
+ `;
562
+
563
+ const SheetContent = styled(Sheet.Content)`
564
+ /* custom styles */
565
+ `;
566
+
567
+ const SheetBackdrop = styled(Sheet.Backdrop)`
568
+ /* custom styles */
569
+ `;
499
570
  ```
500
571
 
572
+ ### Custom styles example
573
+
574
+ You can customize the sheet quite a lot if you get creative with the styles.
575
+
576
+ Here's an example how a totally custom sheet could look like:
577
+
578
+ <p align="center">
579
+ <img width="355" height="768" src="media/example-custom-styles.gif">
580
+ </p>
581
+
582
+ See the [CustomStyles](https://github.com/Temzasse/react-modal-sheet/blob/main/example/src/components/CustomStyles.tsx) component to view the full implementation of the above example.
583
+
584
+ > [!IMPORTANT]
585
+ > Wrapping the sheet components with custom elements or components can have unexpected results since the default sheet styles provided by the library rely on the sheet parts being direct children of each other. Some behavious like keyboard avoidance or scroll handling might not work as expected if the sheet parts are not direct children of each other or if you customize the styles too much.
586
+
501
587
  ## ♿️ Accessibility
502
588
 
503
589
  By default, react-modal-sheet doesn't include any built-in accessibility properties in order to not make any assumptions and to support a wide range of use cases. Additionally, not including 3rd party libraries for features like focus trapping or screen reader support makes it possible to utilize any accessibility libraries that your project may already use, eg. [React Aria](https://react-spectrum.adobe.com/react-aria/getting-started.html). This also helps to reduce JS bloat by not including similar libraries multiple times in your app bundle.
@@ -592,4 +678,12 @@ In your projects it might make sense to build a reusable bottom sheet that has a
592
678
 
593
679
  ### The sheet doesn't open when using `StrictMode`
594
680
 
595
- If you are using React [StrictMode](https://react.dev/reference/react/StrictMode) the sheet animations might not work as expected. This seems to be an issue in the `motion` library and I haven't been able to find a good solution for it yet. You see all `motion` issues related to the `StrictMode` [here](https://github.com/motiondivision/motion/issues?q=is%3Aissue+StrictMode). Easiest solution is to just not to use `StrictMode` 🤷‍♂️
681
+ If you are using React `StrictMode` the sheet animations might not work as expected. This seems to be an issue in the `motion` library and I haven't been able to find a good solution for it yet. You can see all `motion` issues related to the `StrictMode` [here](https://github.com/motiondivision/motion/issues?q=is%3Aissue+StrictMode). Easiest solution is to just not use `StrictMode` 🤷‍♂️
682
+
683
+ ### Upgrading from v4 to v5
684
+
685
+ If you're experiencing issues after upgrading, check the "What's new in v5" section above for breaking changes. The most common issues are:
686
+
687
+ 1. **Snap points not working**: Ensure your snap points are in ascending order (`[0, 0.5, 1]` instead of `[1, 0.5, 0]`)
688
+ 2. **Scrolling issues**: Remove `Sheet.Scroller` components as scrolling is now handled by `Sheet.Content`
689
+ 3. **Detent errors**: Update detent prop values (`"full-height"` → `"default"`, `"content-height"` → `"content"`)