react-modal-sheet 4.4.0 → 5.1.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 +332 -188
- package/dist/index.d.mts +51 -39
- package/dist/index.d.ts +51 -39
- package/dist/index.js +854 -585
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +847 -579
- package/dist/index.mjs.map +1 -1
- package/package.json +5 -3
- package/src/SheetBackdrop.tsx +19 -6
- package/src/SheetContainer.tsx +34 -25
- package/src/SheetContent.tsx +77 -14
- package/src/SheetDragIndicator.tsx +52 -0
- package/src/SheetHeader.tsx +30 -30
- package/src/constants.ts +5 -3
- package/src/context.tsx +2 -42
- package/src/hooks/use-dimensions.ts +4 -4
- package/src/hooks/use-drag-constraints.ts +3 -3
- package/src/hooks/use-modal-effect.ts +19 -23
- package/src/hooks/use-resize-observer.ts +31 -0
- package/src/hooks/use-scroll-position.ts +72 -0
- package/src/hooks/use-sheet-state.ts +63 -0
- package/src/hooks/use-virtual-keyboard.ts +153 -0
- package/src/index.tsx +14 -10
- package/src/sheet.tsx +247 -209
- package/src/snap.ts +226 -0
- package/src/styles.ts +94 -53
- package/src/types.tsx +83 -79
- package/src/utils.ts +8 -149
- package/src/SheetScroller.tsx +0 -96
- package/src/hooks/use-event-callbacks.ts +0 -44
package/README.md
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<p align='center'>
|
|
2
|
-
<img src="media/
|
|
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
|
-
|  |  |  |  |
|
|
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,37 @@ 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`, `
|
|
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
|
|
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
|
-
| `
|
|
93
|
-
| `
|
|
94
|
-
| `
|
|
95
|
-
| `
|
|
96
|
-
| `
|
|
97
|
-
| `
|
|
98
|
-
| `
|
|
99
|
-
| `
|
|
100
|
-
| `
|
|
101
|
-
| `
|
|
102
|
-
| `
|
|
103
|
-
| `
|
|
104
|
-
| `
|
|
105
|
-
| `
|
|
106
|
-
| `
|
|
107
|
-
| `
|
|
108
|
-
| `
|
|
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. |
|
|
141
|
+
| `unstyled` | no | false | Remove all decorative styles and only apply base functional styles. Useful when you want to completely customize the sheet appearance. |
|
|
109
142
|
|
|
110
143
|
## ⚙️ Methods and properties
|
|
111
144
|
|
|
@@ -117,9 +150,9 @@ Imperative method that can be accessed via a ref for snapping to a snap point in
|
|
|
117
150
|
import { Sheet, SheetRef } from 'react-modal-sheet';
|
|
118
151
|
import { useState, useRef } from 'react';
|
|
119
152
|
|
|
120
|
-
const snapPoints = [
|
|
153
|
+
const snapPoints = [0, 0.5, 1];
|
|
121
154
|
|
|
122
|
-
function
|
|
155
|
+
function SnapExample() {
|
|
123
156
|
const [isOpen, setOpen] = useState(false);
|
|
124
157
|
const ref = useRef<SheetRef>(null);
|
|
125
158
|
const snapTo = (i: number) => ref.current?.snapTo(i);
|
|
@@ -144,7 +177,6 @@ function Example() {
|
|
|
144
177
|
<button onClick={() => snapTo(0)}>Snap to index 0</button>
|
|
145
178
|
<button onClick={() => snapTo(1)}>Snap to index 1</button>
|
|
146
179
|
<button onClick={() => snapTo(2)}>Snap to index 2</button>
|
|
147
|
-
<button onClick={() => snapTo(3)}>Snap to index 3</button>
|
|
148
180
|
</Sheet.Content>
|
|
149
181
|
</Sheet.Container>
|
|
150
182
|
</Sheet>
|
|
@@ -153,49 +185,43 @@ function Example() {
|
|
|
153
185
|
}
|
|
154
186
|
```
|
|
155
187
|
|
|
156
|
-
### Motion
|
|
188
|
+
### Motion values `y`, `yInverted` and `height`
|
|
189
|
+
|
|
190
|
+
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
191
|
|
|
158
|
-
The `
|
|
192
|
+
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
193
|
|
|
160
|
-
|
|
194
|
+
The `height` value represents the current height of the sheet in pixels.
|
|
161
195
|
|
|
162
|
-
|
|
196
|
+
All these values can be accessed via a ref, similar to the `snapTo` method.
|
|
163
197
|
|
|
164
|
-
Below you can see
|
|
198
|
+
Below you can see an example of how to use these values:
|
|
165
199
|
|
|
166
200
|
```tsx
|
|
167
201
|
import { Sheet, SheetRef } from 'react-modal-sheet';
|
|
168
202
|
import { useState, useRef } from 'react';
|
|
169
203
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
function Example() {
|
|
204
|
+
function RefExample() {
|
|
173
205
|
const [isOpen, setOpen] = useState(false);
|
|
174
206
|
const ref = useRef<SheetRef>(null);
|
|
175
207
|
|
|
208
|
+
function doSomething() {
|
|
209
|
+
console.log('> Current y value:', ref.current?.y.get());
|
|
210
|
+
console.log('> Current yInverted value:', ref.current?.yInverted.get());
|
|
211
|
+
console.log('> Current height value:', ref.current?.height);
|
|
212
|
+
}
|
|
213
|
+
|
|
176
214
|
return (
|
|
177
215
|
<>
|
|
178
216
|
<button onClick={() => setOpen(true)}>Open sheet</button>
|
|
179
217
|
|
|
180
|
-
<Sheet
|
|
181
|
-
isOpen={isOpen}
|
|
182
|
-
onClose={() => setOpen(false)}
|
|
183
|
-
initialSnap={1}
|
|
184
|
-
snapPoints={snapPoints}
|
|
185
|
-
>
|
|
218
|
+
<Sheet ref={ref} isOpen={isOpen} onClose={() => setOpen(false)}>
|
|
186
219
|
<Sheet.Container>
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
* an offset that ensures that the sheet content can be scrolled all the
|
|
193
|
-
* way to the bottom in every snap point.
|
|
194
|
-
*/}
|
|
195
|
-
<Sheet.Content style={{ paddingBottom: ref.current?.y }}>
|
|
196
|
-
<Sheet.Scroller draggableAt="both">
|
|
197
|
-
{/* Some content here that makes the sheet content scrollable */}
|
|
198
|
-
</Sheet.Scroller>
|
|
220
|
+
<Sheet.Content>
|
|
221
|
+
<motion.div style={{ paddingBottom: ref.current?.y }}>
|
|
222
|
+
Use animated y value in some way
|
|
223
|
+
</motion.div>
|
|
224
|
+
{/* Your content here */}
|
|
199
225
|
</Sheet.Content>
|
|
200
226
|
</Sheet.Container>
|
|
201
227
|
</Sheet>
|
|
@@ -204,30 +230,21 @@ function Example() {
|
|
|
204
230
|
}
|
|
205
231
|
```
|
|
206
232
|
|
|
207
|
-
> [!NOTE]
|
|
208
|
-
> As this use case is quite a common one, the `Sheet.Scroller` component has a built-in prop called `autoPadding` which automatically keeps the bottom padding in sync with the animated `y` value.
|
|
209
|
-
>
|
|
210
|
-
> See [Automatic padding with snap points](#automatic-padding-with-snap-points) for more details.
|
|
211
|
-
|
|
212
233
|
### Detents
|
|
213
234
|
|
|
214
|
-
By default the sheet will take the full height of the page minus top padding and safe area inset.
|
|
235
|
+
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:
|
|
215
236
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
237
|
+
- `"default"` - Sheet takes full height minus safe areas (default behavior)
|
|
238
|
+
- `"content"` - Sheet height is based on its content
|
|
239
|
+
- `"full"` - Sheet takes the entire viewport height with no safe area insets
|
|
219
240
|
|
|
241
|
+
```tsx
|
|
242
|
+
function DetentExample() {
|
|
220
243
|
return (
|
|
221
|
-
<Sheet
|
|
222
|
-
isOpen={isOpen}
|
|
223
|
-
onClose={() => setOpen(false)}
|
|
224
|
-
detent="content-height"
|
|
225
|
-
>
|
|
244
|
+
<Sheet detent="content">
|
|
226
245
|
<Sheet.Container>
|
|
227
246
|
<Sheet.Content>
|
|
228
|
-
<
|
|
229
|
-
<div style={{ height: 200 }}>Some content</div>
|
|
230
|
-
</Sheet.Scroller>
|
|
247
|
+
<div style={{ height: 200 }}>Some content</div>
|
|
231
248
|
</Sheet.Content>
|
|
232
249
|
</Sheet.Container>
|
|
233
250
|
</Sheet>
|
|
@@ -235,9 +252,9 @@ function Example() {
|
|
|
235
252
|
}
|
|
236
253
|
```
|
|
237
254
|
|
|
238
|
-
|
|
255
|
+
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.
|
|
239
256
|
|
|
240
|
-
It is possible to use snap points with `detent="content
|
|
257
|
+
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.
|
|
241
258
|
|
|
242
259
|
> ℹ️ 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).
|
|
243
260
|
|
|
@@ -267,31 +284,48 @@ Sheet header acts as a drag target and has a dragging direction indicator. Rende
|
|
|
267
284
|
| ------------- | -------- | ------- | ---------------------------------- |
|
|
268
285
|
| `disableDrag` | no | false | Disable drag for the sheet header. |
|
|
269
286
|
|
|
270
|
-
### `Sheet.
|
|
287
|
+
### `Sheet.DragIndicator`
|
|
271
288
|
|
|
272
|
-
Sheet
|
|
289
|
+
Sheet drag indicator renders two small animated lines that indicate the dragging direction. This component is automatically included in `Sheet.Header` but can be used standalone for custom header designs. The indicator lines animate with the sheet's drag state to provide visual feedback.
|
|
273
290
|
|
|
274
|
-
> 🖥 Rendered element: `motion.
|
|
291
|
+
> 🖥 Rendered element: Two `motion.span` elements.
|
|
275
292
|
|
|
276
|
-
####
|
|
293
|
+
#### Usage example
|
|
277
294
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
295
|
+
```tsx
|
|
296
|
+
function CustomHeaderExample() {
|
|
297
|
+
return (
|
|
298
|
+
<Sheet>
|
|
299
|
+
<Sheet.Container>
|
|
300
|
+
<Sheet.Header>
|
|
301
|
+
<div style={{ padding: '16px', textAlign: 'center' }}>
|
|
302
|
+
<h2>Custom Header</h2>
|
|
303
|
+
<Sheet.DragIndicator />
|
|
304
|
+
</div>
|
|
305
|
+
</Sheet.Header>
|
|
306
|
+
<Sheet.Content>
|
|
307
|
+
<p>Sheet content goes here...</p>
|
|
308
|
+
</Sheet.Content>
|
|
309
|
+
</Sheet.Container>
|
|
310
|
+
<Sheet.Backdrop />
|
|
311
|
+
</Sheet>
|
|
312
|
+
);
|
|
313
|
+
}
|
|
314
|
+
```
|
|
281
315
|
|
|
282
|
-
### `Sheet.
|
|
316
|
+
### `Sheet.Content`
|
|
283
317
|
|
|
284
|
-
Sheet
|
|
318
|
+
Sheet content acts as a drag target and handles scrollable content internally. It automatically manages scroll behavior and keyboard avoidance.
|
|
285
319
|
|
|
286
|
-
> 🖥 Rendered element: `motion.div
|
|
320
|
+
> 🖥 Rendered element: `motion.div` (with internal scroller).
|
|
287
321
|
|
|
288
|
-
####
|
|
322
|
+
#### Content props
|
|
289
323
|
|
|
290
|
-
| Name | Required | Default | Description
|
|
291
|
-
| --------------- | -------- | ------- |
|
|
292
|
-
| `
|
|
293
|
-
| `disableScroll` | no | false | Disable scrolling.
|
|
294
|
-
| `
|
|
324
|
+
| Name | Required | Default | Description |
|
|
325
|
+
| --------------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
326
|
+
| `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. |
|
|
327
|
+
| `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. |
|
|
328
|
+
| `scrollRef` | no | | Optional ref to the internal scroll container for accessing scroll methods. |
|
|
295
329
|
|
|
296
330
|
### `Sheet.Backdrop`
|
|
297
331
|
|
|
@@ -301,37 +335,48 @@ Sheet backdrop is a translucent overlay that helps to separate the sheet from it
|
|
|
301
335
|
|
|
302
336
|
> 🖥 Rendered element: `motion.div` or `motion.button`.
|
|
303
337
|
|
|
304
|
-
## ✨ Advanced
|
|
338
|
+
## ✨ Advanced behaviors
|
|
305
339
|
|
|
306
340
|
### ⌨️ Virtual keyboard avoidance
|
|
307
341
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
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.
|
|
311
|
-
|
|
312
|
-
**⚠️ Note: This library does not provide any official built-in solution for keyboard avoidance!**
|
|
342
|
+
React Modal Sheet v5 includes built-in virtual keyboard avoidance that works automatically on mobile devices.
|
|
343
|
+
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.
|
|
313
344
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
345
|
+
```tsx
|
|
346
|
+
function KeyboardExample() {
|
|
347
|
+
return (
|
|
348
|
+
<Sheet avoidKeyboard>
|
|
349
|
+
<Sheet.Container>
|
|
350
|
+
<Sheet.Header />
|
|
351
|
+
<Sheet.Content>
|
|
352
|
+
<input placeholder="Your input here" />
|
|
353
|
+
<textarea placeholder="Your message here" />
|
|
354
|
+
{/* More form elements */}
|
|
355
|
+
</Sheet.Content>
|
|
356
|
+
</Sheet.Container>
|
|
357
|
+
<Sheet.Backdrop />
|
|
358
|
+
</Sheet>
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
```
|
|
319
362
|
|
|
320
|
-
|
|
363
|
+
The keyboard avoidance feature:
|
|
321
364
|
|
|
322
|
-
|
|
365
|
+
- 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
|
|
366
|
+
- Works automatically on mobile devices with virtual keyboard support
|
|
367
|
+
- Applies dynamic padding to keep content visible when the keyboard opens
|
|
368
|
+
- Disables drag gestures while the keyboard is open to prevent glitches
|
|
323
369
|
|
|
324
|
-
|
|
370
|
+
If you need to disable keyboard avoidance, set `avoidKeyboard={false}`.
|
|
325
371
|
|
|
326
|
-
|
|
372
|
+
> [!NOTE]
|
|
373
|
+
> 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.
|
|
327
374
|
|
|
328
|
-
|
|
329
|
-
2. Enable dragging when the scroller is scrolled all the way to the bottom - it is at the `bottom` position.
|
|
330
|
-
3. Enable dragging in `both` positions.
|
|
375
|
+
### 📱 Scrolling behavior
|
|
331
376
|
|
|
332
|
-
The
|
|
377
|
+
The `Sheet.Content` component manages scroll interactions internally and provides several ways to control scrolling:
|
|
333
378
|
|
|
334
|
-
|
|
379
|
+
#### Basic scrollable content
|
|
335
380
|
|
|
336
381
|
```tsx
|
|
337
382
|
function ScrollableExample() {
|
|
@@ -340,7 +385,8 @@ function ScrollableExample() {
|
|
|
340
385
|
<Sheet.Container>
|
|
341
386
|
<Sheet.Header />
|
|
342
387
|
<Sheet.Content>
|
|
343
|
-
|
|
388
|
+
{/* Long content that needs scrolling */}
|
|
389
|
+
<div style={{ height: '200vh' }}>Long content here...</div>
|
|
344
390
|
</Sheet.Content>
|
|
345
391
|
</Sheet.Container>
|
|
346
392
|
<Sheet.Backdrop />
|
|
@@ -349,31 +395,23 @@ function ScrollableExample() {
|
|
|
349
395
|
}
|
|
350
396
|
```
|
|
351
397
|
|
|
352
|
-
####
|
|
398
|
+
#### Controlling scroll behavior with snap points
|
|
353
399
|
|
|
354
|
-
You can
|
|
355
|
-
|
|
356
|
-
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:
|
|
400
|
+
You can disable scrolling conditionally using the `disableScroll` prop. This is useful when you want content to be scrollable only at certain snap points:
|
|
357
401
|
|
|
358
402
|
```tsx
|
|
359
|
-
const snapPoints = [
|
|
360
|
-
const initialSnap = 1;
|
|
361
|
-
|
|
362
|
-
function DisableScrollExample() {
|
|
363
|
-
const [currentSnap, setCurrentSnap] = useState(initialSnap);
|
|
403
|
+
const snapPoints = [0, 0.5, 1];
|
|
364
404
|
|
|
405
|
+
function ConditionalScrollExample() {
|
|
365
406
|
return (
|
|
366
|
-
<Sheet
|
|
367
|
-
initialSnap={initialSnap}
|
|
368
|
-
snapPoints={snapPoints}
|
|
369
|
-
onSnap={setCurrentSnap}
|
|
370
|
-
>
|
|
407
|
+
<Sheet snapPoints={snapPoints} initialSnap={1}>
|
|
371
408
|
<Sheet.Container>
|
|
372
409
|
<Sheet.Header />
|
|
373
|
-
<Sheet.Content
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
410
|
+
<Sheet.Content
|
|
411
|
+
// Only allow scrolling when at the top snap point (index 2)
|
|
412
|
+
disableScroll={(state) => state.currentSnap !== 2}
|
|
413
|
+
>
|
|
414
|
+
<div style={{ height: '200vh' }}>Long content here...</div>
|
|
377
415
|
</Sheet.Content>
|
|
378
416
|
</Sheet.Container>
|
|
379
417
|
<Sheet.Backdrop />
|
|
@@ -382,28 +420,21 @@ function DisableScrollExample() {
|
|
|
382
420
|
}
|
|
383
421
|
```
|
|
384
422
|
|
|
385
|
-
####
|
|
423
|
+
#### Dynamic drag control
|
|
386
424
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
This will automatically apply padding to the bottom of the scroller based on the dragged distance to ensure that content can be scrolled to the bottom at any snap point.
|
|
425
|
+
Similarly, you can control when dragging is enabled based on scroll position or snap point:
|
|
390
426
|
|
|
391
427
|
```tsx
|
|
392
|
-
|
|
393
|
-
import { useState } from 'react';
|
|
394
|
-
|
|
395
|
-
const snapPoints = [-50, 0.5, 0];
|
|
396
|
-
const initialSnap = 1;
|
|
397
|
-
|
|
398
|
-
function AutoPaddingExample() {
|
|
428
|
+
function DynamicDragExample() {
|
|
399
429
|
return (
|
|
400
|
-
<Sheet
|
|
430
|
+
<Sheet>
|
|
401
431
|
<Sheet.Container>
|
|
402
432
|
<Sheet.Header />
|
|
403
|
-
<Sheet.Content
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
433
|
+
<Sheet.Content
|
|
434
|
+
// Disable drag when not scrolled to top
|
|
435
|
+
disableDrag={(state) => state.scrollPosition !== 'top'}
|
|
436
|
+
>
|
|
437
|
+
<div style={{ height: '200vh' }}>Long content here...</div>
|
|
407
438
|
</Sheet.Content>
|
|
408
439
|
</Sheet.Container>
|
|
409
440
|
<Sheet.Backdrop />
|
|
@@ -412,15 +443,17 @@ function AutoPaddingExample() {
|
|
|
412
443
|
}
|
|
413
444
|
```
|
|
414
445
|
|
|
415
|
-
|
|
416
|
-
|
|
446
|
+
The scroll state provides:
|
|
447
|
+
|
|
448
|
+
- `scrollPosition`: `'top'`, `'bottom'`, `'middle'`, or `undefined`
|
|
449
|
+
- `currentSnap`: Current snap point index (if using snap points)
|
|
417
450
|
|
|
418
451
|
### 🪟 iOS Modal View effect
|
|
419
452
|
|
|
420
453
|
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.
|
|
421
454
|
|
|
422
|
-
|  |  |
|
|
456
|
+
| :-------------------------------------------------------: | :-------------------------------------------------------: |
|
|
424
457
|
|
|
425
458
|
To enable this effect you can provide the id of the root element where your application is mounted:
|
|
426
459
|
|
|
@@ -475,8 +508,100 @@ function Example() {
|
|
|
475
508
|
|
|
476
509
|
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.
|
|
477
510
|
|
|
511
|
+
#### Unstyled mode
|
|
512
|
+
|
|
513
|
+
For complete control over the sheet's appearance, you can use the `unstyled` prop to remove all decorative styles while keeping only the essential functional styles.
|
|
514
|
+
|
|
515
|
+
When `unstyled={true}` is set on the root `Sheet` component, it removes decorative styles like: border radius, shadows, and background colors:
|
|
516
|
+
|
|
517
|
+
```tsx
|
|
518
|
+
function UnstyledExample() {
|
|
519
|
+
return (
|
|
520
|
+
<Sheet unstyled>
|
|
521
|
+
<Sheet.Container>
|
|
522
|
+
<Sheet.Header />
|
|
523
|
+
<Sheet.Content>{/* Your content */}</Sheet.Content>
|
|
524
|
+
</Sheet.Container>
|
|
525
|
+
<Sheet.Backdrop />
|
|
526
|
+
</Sheet>
|
|
527
|
+
);
|
|
528
|
+
}
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
The `unstyled` prop can also be applied to individual sheet components to selectively remove their decorative styles:
|
|
532
|
+
|
|
533
|
+
```tsx
|
|
534
|
+
function SelectiveUnstyledExample() {
|
|
535
|
+
return (
|
|
536
|
+
<Sheet>
|
|
537
|
+
<Sheet.Container unstyled>
|
|
538
|
+
<Sheet.Header />
|
|
539
|
+
<Sheet.Content>{/* Your content */}</Sheet.Content>
|
|
540
|
+
</Sheet.Container>
|
|
541
|
+
{/* Keep the backdrop styled */}
|
|
542
|
+
<Sheet.Backdrop />
|
|
543
|
+
</Sheet>
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
```
|
|
547
|
+
|
|
548
|
+
This is particularly useful when building custom designs or when integrating with design systems that require complete control over styling.
|
|
549
|
+
|
|
550
|
+
You can also remove all styles at the root level and then override it for individual components if you want to keep some default styles:
|
|
551
|
+
|
|
552
|
+
```tsx
|
|
553
|
+
function MixedUnstyledExample() {
|
|
554
|
+
return (
|
|
555
|
+
<Sheet unstyled>
|
|
556
|
+
<Sheet.Container>
|
|
557
|
+
<Sheet.Header />
|
|
558
|
+
<Sheet.Content>{/* Your content */}</Sheet.Content>
|
|
559
|
+
</Sheet.Container>
|
|
560
|
+
<Sheet.Backdrop unstyled={false} />
|
|
561
|
+
</Sheet>
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
### CSS Modules
|
|
567
|
+
|
|
568
|
+
```tsx
|
|
569
|
+
import styles from './styles.css';
|
|
570
|
+
|
|
571
|
+
function Example() {
|
|
572
|
+
return (
|
|
573
|
+
<Sheet>
|
|
574
|
+
<Sheet.Container className={styles.sheetContainer}>
|
|
575
|
+
<Sheet.Header className={styles.sheetHeader} />
|
|
576
|
+
<Sheet.Content className={styles.sheetContent}>{/*...*/}</Sheet.Content>
|
|
577
|
+
</Sheet.Container>
|
|
578
|
+
<Sheet.Backdrop className={styles.sheetBackdrop} />
|
|
579
|
+
</Sheet>
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
```
|
|
583
|
+
|
|
584
|
+
```css
|
|
585
|
+
/* styles.css */
|
|
586
|
+
|
|
587
|
+
.sheetContainer {
|
|
588
|
+
/* custom styles */
|
|
589
|
+
}
|
|
590
|
+
.sheetHeader {
|
|
591
|
+
/* custom styles */
|
|
592
|
+
}
|
|
593
|
+
.sheetContent {
|
|
594
|
+
/* custom styles */
|
|
595
|
+
}
|
|
596
|
+
.sheetBackdrop {
|
|
597
|
+
/* custom styles */
|
|
598
|
+
}
|
|
599
|
+
```
|
|
600
|
+
|
|
478
601
|
#### Vanilla CSS
|
|
479
602
|
|
|
603
|
+
You can also use vanilla CSS to override the styles by targeting the exposed class names:
|
|
604
|
+
|
|
480
605
|
```css
|
|
481
606
|
.react-modal-sheet-backdrop {
|
|
482
607
|
/* custom styles */
|
|
@@ -487,6 +612,12 @@ You can add your own styles or override the default sheet styles via the exposed
|
|
|
487
612
|
.react-modal-sheet-header {
|
|
488
613
|
/* custom styles */
|
|
489
614
|
}
|
|
615
|
+
.react-modal-sheet-header-container {
|
|
616
|
+
/* custom styles */
|
|
617
|
+
}
|
|
618
|
+
.react-modal-sheet-drag-indicator-container {
|
|
619
|
+
/* custom styles */
|
|
620
|
+
}
|
|
490
621
|
.react-modal-sheet-drag-indicator {
|
|
491
622
|
/* custom styles */
|
|
492
623
|
}
|
|
@@ -498,47 +629,52 @@ You can add your own styles or override the default sheet styles via the exposed
|
|
|
498
629
|
#### CSS-in-JS
|
|
499
630
|
|
|
500
631
|
```tsx
|
|
501
|
-
import {
|
|
502
|
-
import { styled } from 'styled-components';
|
|
503
|
-
import { useState } from 'react';
|
|
504
|
-
|
|
505
|
-
const CustomSheet = styled(Sheet)`
|
|
506
|
-
.react-modal-sheet-backdrop {
|
|
507
|
-
/* custom styles */
|
|
508
|
-
}
|
|
509
|
-
.react-modal-sheet-container {
|
|
510
|
-
/* custom styles */
|
|
511
|
-
}
|
|
512
|
-
.react-modal-sheet-header {
|
|
513
|
-
/* custom styles */
|
|
514
|
-
}
|
|
515
|
-
.react-modal-sheet-drag-indicator {
|
|
516
|
-
/* custom styles */
|
|
517
|
-
}
|
|
518
|
-
.react-modal-sheet-content {
|
|
519
|
-
/* custom styles */
|
|
520
|
-
}
|
|
521
|
-
`;
|
|
632
|
+
import { styled } from 'styled-components'; // or emotion, pandacss, etc.
|
|
522
633
|
|
|
523
634
|
function Example() {
|
|
524
|
-
const [isOpen, setOpen] = useState(false);
|
|
525
|
-
|
|
526
635
|
return (
|
|
527
|
-
|
|
528
|
-
<
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
</Sheet.Container>
|
|
535
|
-
<Sheet.Backdrop />
|
|
536
|
-
</CustomSheet>
|
|
537
|
-
</>
|
|
636
|
+
<Sheet>
|
|
637
|
+
<SheetContainer>
|
|
638
|
+
<SheetHeader />
|
|
639
|
+
<SheetContent>{/*...*/}</SheetContent>
|
|
640
|
+
</SheetContainer>
|
|
641
|
+
<SheetBackdrop />
|
|
642
|
+
</Sheet>
|
|
538
643
|
);
|
|
539
644
|
}
|
|
645
|
+
|
|
646
|
+
const SheetContainer = styled(Sheet.Container)`
|
|
647
|
+
/* custom styles */
|
|
648
|
+
`;
|
|
649
|
+
|
|
650
|
+
const SheetHeader = styled(Sheet.Header)`
|
|
651
|
+
/* custom styles */
|
|
652
|
+
`;
|
|
653
|
+
|
|
654
|
+
const SheetContent = styled(Sheet.Content)`
|
|
655
|
+
/* custom styles */
|
|
656
|
+
`;
|
|
657
|
+
|
|
658
|
+
const SheetBackdrop = styled(Sheet.Backdrop)`
|
|
659
|
+
/* custom styles */
|
|
660
|
+
`;
|
|
540
661
|
```
|
|
541
662
|
|
|
663
|
+
### Custom styles example
|
|
664
|
+
|
|
665
|
+
You can customize the sheet quite a lot if you get creative with the styles.
|
|
666
|
+
|
|
667
|
+
Here's an example how a totally custom sheet could look like:
|
|
668
|
+
|
|
669
|
+
<p align="center">
|
|
670
|
+
<img width="355" height="768" src="media/example-custom-styles.gif">
|
|
671
|
+
</p>
|
|
672
|
+
|
|
673
|
+
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.
|
|
674
|
+
|
|
675
|
+
> [!IMPORTANT]
|
|
676
|
+
> 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.
|
|
677
|
+
|
|
542
678
|
## ♿️ Accessibility
|
|
543
679
|
|
|
544
680
|
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.
|
|
@@ -633,4 +769,12 @@ In your projects it might make sense to build a reusable bottom sheet that has a
|
|
|
633
769
|
|
|
634
770
|
### The sheet doesn't open when using `StrictMode`
|
|
635
771
|
|
|
636
|
-
If you are using React
|
|
772
|
+
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` 🤷♂️
|
|
773
|
+
|
|
774
|
+
### Upgrading from v4 to v5
|
|
775
|
+
|
|
776
|
+
If you're experiencing issues after upgrading, check the "What's new in v5" section above for breaking changes. The most common issues are:
|
|
777
|
+
|
|
778
|
+
1. **Snap points not working**: Ensure your snap points are in ascending order (`[0, 0.5, 1]` instead of `[1, 0.5, 0]`)
|
|
779
|
+
2. **Scrolling issues**: Remove `Sheet.Scroller` components as scrolling is now handled by `Sheet.Content`
|
|
780
|
+
3. **Detent errors**: Update detent prop values (`"full-height"` → `"default"`, `"content-height"` → `"content"`)
|