@surdeddd/bottom-sheet 0.23.3 → 0.24.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 +513 -525
- package/README.ru.md +685 -0
- package/dist/{BottomSheetCore-COvILxmR.d.ts → BottomSheetCore-B_r4KCQL.d.cts} +48 -12
- package/dist/{BottomSheetCore-CaJwHnxY.d.cts → BottomSheetCore-B_r4KCQL.d.ts} +48 -12
- package/dist/{BottomSheetEngine-C_1aPJsU.d.cts → BottomSheetEngine-0F7mjyvF.d.cts} +1 -1
- package/dist/{BottomSheetEngine-CsgCcCSO.d.ts → BottomSheetEngine-CJV_DDrM.d.ts} +1 -1
- package/dist/core.cjs +810 -436
- package/dist/core.d.cts +2 -3
- package/dist/core.d.ts +2 -3
- package/dist/core.js +810 -436
- package/dist/debug.d.cts +3 -3
- package/dist/debug.d.ts +3 -3
- package/dist/{default-features-BJx7J0NH.d.ts → default-features-4Xwv6aVt.d.ts} +4 -2
- package/dist/{default-features-CTJb8Wdk.d.cts → default-features-DX2NOxP6.d.cts} +4 -2
- package/dist/element.cjs +1428 -699
- package/dist/element.d.cts +15 -5
- package/dist/element.d.ts +15 -5
- package/dist/element.iife.global.js +2 -2
- package/dist/element.js +1428 -699
- package/dist/features.cjs +334 -190
- package/dist/features.d.cts +3 -4
- package/dist/features.d.ts +3 -4
- package/dist/features.js +334 -191
- package/dist/{index-CXN7t3rr.d.ts → index-2shlxhcG.d.cts} +3 -2
- package/dist/{index-BBrJJjy5.d.cts → index-CNCRQnpc.d.ts} +3 -2
- package/dist/index.cjs +1129 -631
- package/dist/index.d.cts +13 -7
- package/dist/index.d.ts +13 -7
- package/dist/index.js +1129 -632
- package/dist/integrations/formik.cjs +21 -10
- package/dist/integrations/formik.d.cts +3 -3
- package/dist/integrations/formik.d.ts +3 -3
- package/dist/integrations/formik.js +22 -11
- package/dist/integrations/react-hook-form.cjs +21 -12
- package/dist/integrations/react-hook-form.d.cts +3 -3
- package/dist/integrations/react-hook-form.d.ts +3 -3
- package/dist/integrations/react-hook-form.js +21 -12
- package/dist/option-keys-DItuAjUw.d.cts +6 -0
- package/dist/option-keys-a5DYNwwO.d.ts +6 -0
- package/dist/overlay.cjs +386 -189
- package/dist/overlay.d.cts +3 -4
- package/dist/overlay.d.ts +3 -4
- package/dist/overlay.js +386 -189
- package/dist/preact.d.cts +3 -4
- package/dist/preact.d.ts +3 -4
- package/dist/qwik-lib/index.qwik.cjs +1306 -671
- package/dist/qwik-lib/index.qwik.mjs +1306 -671
- package/dist/qwik.cjs +1326 -701
- package/dist/qwik.d.cts +14 -30
- package/dist/qwik.d.ts +14 -30
- package/dist/qwik.js +1326 -701
- package/dist/react.cjs +1373 -750
- package/dist/react.d.cts +31 -14
- package/dist/react.d.ts +31 -14
- package/dist/react.js +1375 -752
- package/dist/solid.cjs +1332 -727
- package/dist/solid.d.cts +13 -437
- package/dist/solid.d.ts +13 -437
- package/dist/solid.js +1334 -729
- package/dist/solid.source.jsx +1374 -751
- package/dist/styles.css +20 -6
- package/dist/svelte-core.cjs +1211 -676
- package/dist/svelte-core.js +1207 -677
- package/dist/svelte-src/BottomSheet.svelte +102 -59
- package/dist/svelte.cjs +1324 -707
- package/dist/svelte.d.cts +45 -25
- package/dist/svelte.d.ts +45 -25
- package/dist/svelte.js +1324 -707
- package/dist/vue.cjs +1354 -773
- package/dist/vue.d.cts +20 -3
- package/dist/vue.d.ts +20 -3
- package/dist/vue.js +1354 -773
- package/dist/webgl.cjs +38 -7
- package/dist/webgl.d.cts +3 -3
- package/dist/webgl.d.ts +3 -3
- package/dist/webgl.js +38 -7
- package/llms.txt +330 -0
- package/package.json +62 -42
- package/dist/transform-jVfAikE9.d.cts +0 -3
- package/dist/transform-jVfAikE9.d.ts +0 -3
package/README.md
CHANGED
|
@@ -1,57 +1,51 @@
|
|
|
1
1
|
# @surdeddd/bottom-sheet
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
Bottom sheet, drawer and modal sheet for the web. One gesture engine with snap
|
|
4
|
+
points, spring physics and keyboard accessibility, and the same engine behind
|
|
5
|
+
adapters for **React, Vue 3, Svelte 5, Solid, Qwik, Preact** and a
|
|
6
|
+
`<bottom-sheet>` **custom element** that works in Angular, Lit, Astro or plain
|
|
7
|
+
HTML.
|
|
8
|
+
|
|
9
|
+
[](https://www.npmjs.com/package/@surdeddd/bottom-sheet)
|
|
10
|
+
[](https://www.npmjs.com/package/@surdeddd/bottom-sheet)
|
|
11
|
+

|
|
12
|
+

|
|
13
|
+

|
|
9
14
|

|
|
10
15
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
## 🚀 [Live demo → bottom-sheet-demo-ashy.vercel.app](https://bottom-sheet-demo-ashy.vercel.app)
|
|
16
|
-
|
|
17
|
-
📚 [API reference (typedoc) → surdeddd.github.io/BottomSheet](https://surdeddd.github.io/BottomSheet/)
|
|
16
|
+
**[Live demo](https://bottom-sheet-demo-ashy.vercel.app)** ·
|
|
17
|
+
**[API reference](https://surdeddd.github.io/BottomSheet/)** ·
|
|
18
|
+
[Changelog](CHANGELOG.md) ·
|
|
19
|
+
[Русская версия](https://github.com/Surdeddd/BottomSheet/blob/main/README.ru.md)
|
|
18
20
|
|
|
19
21
|
<p align="center">
|
|
20
22
|
<picture>
|
|
21
23
|
<source media="(prefers-color-scheme: dark)" srcset="docs/screenshots/sheet-dark.png" />
|
|
22
|
-
<img src="docs/screenshots/sheet-light.png" alt="bottom sheet at
|
|
24
|
+
<img src="docs/screenshots/sheet-light.png" alt="A bottom sheet at its middle snap point over a dimmed page" width="380" />
|
|
23
25
|
</picture>
|
|
26
|
+
<img src="https://raw.githubusercontent.com/Surdeddd/BottomSheet/main/docs/gifs/01-drag-to-close.gif" alt="Dragging the sheet between snap points and flicking it closed" width="300" />
|
|
24
27
|
</p>
|
|
25
28
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
- **
|
|
43
|
-
|
|
44
|
-
- **
|
|
45
|
-
|
|
46
|
-
indicator.
|
|
47
|
-
- **WCAG 2.1 AA keyboard slider** — Arrow/Home/End/Esc stepping, focus trap
|
|
48
|
-
with restore.
|
|
49
|
-
- **Multi-sheet stacking** plus a typed sheet-manager registry.
|
|
50
|
-
- **Cancelable `before-snap` / `before-close`** — veto a transition
|
|
51
|
-
synchronously.
|
|
52
|
-
- Backed by 500+ unit tests across the suite and ~200 Playwright e2e runs
|
|
53
|
-
across three browser engines (`mobile-chrome` / `mobile-safari` /
|
|
54
|
-
`firefox`).
|
|
29
|
+
## Why this one
|
|
30
|
+
|
|
31
|
+
- **One engine, every framework.** The component you use in React behaves
|
|
32
|
+
exactly like the one in Vue, Svelte, Solid, Qwik or the custom element,
|
|
33
|
+
because they are thin adapters over the same core.
|
|
34
|
+
- **Snap points in any unit.** Pixels, percentages, `fit` (the content's own
|
|
35
|
+
height) or any CSS length such as `50dvh` or `clamp(200px, 60%, 800px)`.
|
|
36
|
+
- **Four edges.** `bottom`, `top`, `left`, `right`, plus `start` / `end` that
|
|
37
|
+
follow the writing direction.
|
|
38
|
+
- **Feels native.** Spring physics with velocity carry, rubber-band over-drag,
|
|
39
|
+
separate thresholds for touch and mouse, motion on the compositor only.
|
|
40
|
+
- **Accessible.** Keyboard slider on the handle, focus trap with restore,
|
|
41
|
+
`inert` for everything behind a modal sheet, reduced motion, forced colors.
|
|
42
|
+
- **Knows about the phone.** Rests on top of the on-screen keyboard, respects
|
|
43
|
+
safe-area insets, closes on the Back button, locks page scroll the iOS-safe
|
|
44
|
+
way.
|
|
45
|
+
- **Stacks.** Several sheets open at once, each with its own backdrop and focus
|
|
46
|
+
scope.
|
|
47
|
+
- **Small and tree-shakable.** 25.6 KB gzip for the full engine, 21.9 KB for the
|
|
48
|
+
slim core, no runtime dependencies.
|
|
55
49
|
|
|
56
50
|
## Install
|
|
57
51
|
|
|
@@ -59,624 +53,618 @@ Every item below is verifiable in this repo's `src/`:
|
|
|
59
53
|
npm i @surdeddd/bottom-sheet
|
|
60
54
|
```
|
|
61
55
|
|
|
62
|
-
|
|
63
|
-
the framework you use, the bundle pays only for that adapter.
|
|
56
|
+
Frameworks are optional peer dependencies: you install only the one you use.
|
|
64
57
|
|
|
65
58
|
## Quick start
|
|
66
59
|
|
|
67
|
-
###
|
|
60
|
+
### A modal sheet opened by a button
|
|
68
61
|
|
|
69
62
|
```tsx
|
|
63
|
+
import { useState } from 'react';
|
|
70
64
|
import { BottomSheet } from '@surdeddd/bottom-sheet/react';
|
|
71
65
|
import '@surdeddd/bottom-sheet/styles';
|
|
72
66
|
|
|
67
|
+
export function Filters() {
|
|
68
|
+
const [open, setOpen] = useState(false);
|
|
69
|
+
|
|
70
|
+
return (
|
|
71
|
+
<>
|
|
72
|
+
<button onClick={() => setOpen(true)}>Filters</button>
|
|
73
|
+
|
|
74
|
+
<BottomSheet
|
|
75
|
+
open={open}
|
|
76
|
+
onOpenChange={setOpen}
|
|
77
|
+
snapPoints={[
|
|
78
|
+
{ id: 'closed', size: 0 },
|
|
79
|
+
{ id: 'open', size: 'fit' },
|
|
80
|
+
]}
|
|
81
|
+
focusTrap
|
|
82
|
+
header={<h2>Filters</h2>}
|
|
83
|
+
>
|
|
84
|
+
<FilterForm />
|
|
85
|
+
</BottomSheet>
|
|
86
|
+
</>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
`open` needs one snap point of size `0`: that is the closed state. A tap on the
|
|
92
|
+
backdrop, Escape and a drag down all close the sheet and report it through
|
|
93
|
+
`onOpenChange`.
|
|
94
|
+
|
|
95
|
+
### A sheet that stays on screen
|
|
96
|
+
|
|
97
|
+
The maps pattern: a strip at the bottom that the user pulls up.
|
|
98
|
+
|
|
99
|
+
```tsx
|
|
73
100
|
<BottomSheet
|
|
74
101
|
snapPoints={[
|
|
75
|
-
{ id: '
|
|
102
|
+
{ id: 'peek', size: 96 },
|
|
76
103
|
{ id: 'half', size: '45dvh' },
|
|
77
|
-
{ id: 'full', size: '
|
|
104
|
+
{ id: 'full', size: '90%' },
|
|
78
105
|
]}
|
|
79
|
-
initial=
|
|
80
|
-
|
|
81
|
-
spring={{ stiffness: 260, damping: 28 }}
|
|
82
|
-
focusTrap
|
|
83
|
-
closeOnEscape
|
|
84
|
-
header={<h2>Search vehicles</h2>}
|
|
106
|
+
initial="peek"
|
|
107
|
+
header={<h2>Nearby</h2>}
|
|
85
108
|
>
|
|
86
|
-
<
|
|
87
|
-
</BottomSheet
|
|
109
|
+
<PlaceList />
|
|
110
|
+
</BottomSheet>
|
|
88
111
|
```
|
|
89
112
|
|
|
90
|
-
|
|
113
|
+
### Other frameworks
|
|
91
114
|
|
|
92
|
-
|
|
115
|
+
<details>
|
|
116
|
+
<summary><b>Vue 3</b></summary>
|
|
93
117
|
|
|
94
118
|
```vue
|
|
95
|
-
<script setup>
|
|
96
|
-
|
|
97
|
-
|
|
119
|
+
<script setup lang="ts">
|
|
120
|
+
import { ref } from 'vue';
|
|
121
|
+
import { BottomSheet } from '@surdeddd/bottom-sheet/vue';
|
|
122
|
+
import '@surdeddd/bottom-sheet/styles';
|
|
123
|
+
|
|
124
|
+
const open = ref(false);
|
|
125
|
+
const points = [
|
|
126
|
+
{ id: 'closed', size: 0 },
|
|
127
|
+
{ id: 'open', size: 'fit' },
|
|
128
|
+
];
|
|
98
129
|
</script>
|
|
99
130
|
|
|
100
131
|
<template>
|
|
101
|
-
<
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
@snap="onSnap"
|
|
106
|
-
>
|
|
107
|
-
<template #header><h2>Search</h2></template>
|
|
108
|
-
<YourList />
|
|
132
|
+
<button @click="open = true">Filters</button>
|
|
133
|
+
<BottomSheet v-model:open="open" :snap-points="points" focus-trap>
|
|
134
|
+
<template #header><h2>Filters</h2></template>
|
|
135
|
+
<FilterForm />
|
|
109
136
|
</BottomSheet>
|
|
110
137
|
</template>
|
|
111
138
|
```
|
|
112
139
|
|
|
113
|
-
[
|
|
140
|
+
[Vue guide →](docs/vue.md)
|
|
114
141
|
|
|
115
|
-
|
|
142
|
+
</details>
|
|
143
|
+
|
|
144
|
+
<details>
|
|
145
|
+
<summary><b>Svelte 5</b></summary>
|
|
116
146
|
|
|
117
147
|
```svelte
|
|
118
148
|
<script lang="ts">
|
|
119
|
-
import {
|
|
120
|
-
import
|
|
121
|
-
|
|
122
|
-
let sheetEl: HTMLElement | undefined = $state();
|
|
123
|
-
let handleEl: HTMLElement | undefined = $state();
|
|
124
|
-
let contentEl: HTMLElement | undefined = $state();
|
|
125
|
-
|
|
126
|
-
const ctrl = createBottomSheet({
|
|
127
|
-
snapPoints: [{ id: "min", size: 96 }, { id: "full", size: "85%" }],
|
|
128
|
-
initial: "min",
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
$effect(() => {
|
|
132
|
-
if (!sheetEl) return;
|
|
133
|
-
return ctrl.attach({ element: sheetEl, handle: handleEl, scrollContainer: contentEl });
|
|
134
|
-
});
|
|
135
|
-
</script>
|
|
136
|
-
|
|
137
|
-
<section class="bs-sheet" bind:this={sheetEl} data-mode="bottom">
|
|
138
|
-
<div class="bs-handle" bind:this={handleEl}><h2>Search</h2></div>
|
|
139
|
-
<div class="bs-content" bind:this={contentEl}><!-- list --></div>
|
|
140
|
-
</section>
|
|
141
|
-
```
|
|
149
|
+
import { BottomSheet } from '@surdeddd/bottom-sheet/svelte';
|
|
150
|
+
import '@surdeddd/bottom-sheet/styles';
|
|
142
151
|
|
|
143
|
-
|
|
152
|
+
let open = $state(false);
|
|
153
|
+
</script>
|
|
144
154
|
|
|
145
|
-
|
|
155
|
+
<button onclick={() => (open = true)}>Filters</button>
|
|
146
156
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
<bottom-sheet
|
|
152
|
-
snap-points='[{"id":"min","size":96},{"id":"full","size":"85%"}]'
|
|
153
|
-
initial="min"
|
|
154
|
-
animation="spring"
|
|
155
|
-
focus-trap="true"
|
|
157
|
+
<BottomSheet
|
|
158
|
+
bind:open
|
|
159
|
+
snapPoints={[{ id: 'closed', size: 0 }, { id: 'open', size: 'fit' }]}
|
|
160
|
+
focusTrap
|
|
156
161
|
>
|
|
157
|
-
<h2
|
|
158
|
-
<
|
|
159
|
-
|
|
160
|
-
</ul>
|
|
161
|
-
</bottom-sheet>
|
|
162
|
+
{#snippet header()}<h2>Filters</h2>{/snippet}
|
|
163
|
+
{#snippet children()}<FilterForm />{/snippet}
|
|
164
|
+
</BottomSheet>
|
|
162
165
|
```
|
|
163
166
|
|
|
164
|
-
[
|
|
167
|
+
[Svelte guide →](docs/svelte.md)
|
|
165
168
|
|
|
166
|
-
|
|
169
|
+
</details>
|
|
167
170
|
|
|
168
|
-
|
|
169
|
-
|
|
171
|
+
<details>
|
|
172
|
+
<summary><b>Solid</b></summary>
|
|
170
173
|
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
rel="stylesheet"
|
|
175
|
-
href="https://unpkg.com/@surdeddd/bottom-sheet/dist/styles.css"
|
|
176
|
-
/>
|
|
177
|
-
<script src="https://unpkg.com/@surdeddd/bottom-sheet/dist/element.iife.global.js"></script>
|
|
178
|
-
<bottom-sheet
|
|
179
|
-
snap-points='[{"id":"min","size":96},{"id":"full","size":"85%"}]'
|
|
180
|
-
initial="min"
|
|
181
|
-
>
|
|
182
|
-
<h2 slot="header">Title</h2>
|
|
183
|
-
<p>Content</p>
|
|
184
|
-
</bottom-sheet>
|
|
185
|
-
```
|
|
186
|
-
|
|
187
|
-
### Vanilla core (no framework)
|
|
188
|
-
|
|
189
|
-
```ts
|
|
190
|
-
import { BottomSheetEngine } from '@surdeddd/bottom-sheet';
|
|
174
|
+
```tsx
|
|
175
|
+
import { createSignal } from 'solid-js';
|
|
176
|
+
import { BottomSheet } from '@surdeddd/bottom-sheet/solid';
|
|
191
177
|
import '@surdeddd/bottom-sheet/styles';
|
|
192
178
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
|
|
179
|
+
export function Filters() {
|
|
180
|
+
const [open, setOpen] = createSignal(false);
|
|
181
|
+
return (
|
|
182
|
+
<>
|
|
183
|
+
<button onClick={() => setOpen(true)}>Filters</button>
|
|
184
|
+
<BottomSheet
|
|
185
|
+
open={open()}
|
|
186
|
+
onOpenChange={setOpen}
|
|
187
|
+
snapPoints={[{ id: 'closed', size: 0 }, { id: 'open', size: 'fit' }]}
|
|
188
|
+
focusTrap
|
|
189
|
+
header={<h2>Filters</h2>}
|
|
190
|
+
>
|
|
191
|
+
<FilterForm />
|
|
192
|
+
</BottomSheet>
|
|
193
|
+
</>
|
|
194
|
+
);
|
|
195
|
+
}
|
|
205
196
|
```
|
|
206
197
|
|
|
207
|
-
[
|
|
208
|
-
|
|
209
|
-
## API at a glance
|
|
210
|
-
|
|
211
|
-
### Snap points
|
|
212
|
-
|
|
213
|
-
```ts
|
|
214
|
-
type SnapPoint =
|
|
215
|
-
| number // pixels
|
|
216
|
-
| `${number}%` // percent of viewport along the axis
|
|
217
|
-
| 'fit' // natural height of the whole sheet (handle + header + content + footer)
|
|
218
|
-
| 'content' // alias of 'fit'
|
|
219
|
-
| 'full' // 100 % of axis
|
|
220
|
-
| string; // any CSS length — "50dvh", "clamp(200px, 60%, 800px)"
|
|
221
|
-
```
|
|
198
|
+
[Solid guide →](docs/solid.md)
|
|
222
199
|
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
| Option | Default | What it does |
|
|
226
|
-
| -------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
227
|
-
| `snapPoints` | required | Ordered list of `{ id, size }` |
|
|
228
|
-
| `allowed` | all ids | Subset the sheet may settle on right now |
|
|
229
|
-
| `initial` | first allowed | Snap id to start at |
|
|
230
|
-
| `mode` | `"bottom"` | `bottom`, `top`, `left`, `right`, or logical `start` / `end`; reactive in React and Vue adapters |
|
|
231
|
-
| `animation` | `"spring"` | `spring`, `tween`, `ios-spring`, `material-bounce`, `linear`, `snappy` |
|
|
232
|
-
| `settleAnimation` | off | `"waapi"` — settle runs as a compositor-driven Web Animation (pre-sampled spring keyframes); falls back to rAF automatically |
|
|
233
|
-
| `features` | all defaults | `EngineFeature[]` — override/extend the composable feature set (see Slim core below) |
|
|
234
|
-
| `spring` | snappy | `{ stiffness, damping, mass }` |
|
|
235
|
-
| `flickVelocity` | `0.65 px/ms` | Mobile flick threshold |
|
|
236
|
-
| `dragThreshold` | `18 px` | Below this, drag snaps back |
|
|
237
|
-
| `rubberBand` | `true` | iOS-style soft over-drag |
|
|
238
|
-
| `backdropRange` | `[0, 1]` | Progress range over which backdrop fades |
|
|
239
|
-
| `screenRange` | `[0, 1]` | Progress range over which the `screen`/scrim content fades |
|
|
240
|
-
| `focusTrap` | `false` | Trap Tab focus inside the sheet when open |
|
|
241
|
-
| `initialFocus` | first field | Element / selector to focus on open; `false` focuses the sheet container (`tabindex=-1`) instead — avoids the iOS keyboard |
|
|
242
|
-
| `closeOnEscape` | `true` | Listen for Escape and close |
|
|
243
|
-
| `closeOnBack` | `false` | Intercept Android back button (history.popstate) |
|
|
244
|
-
| `closeOnRouteChange` | `false` | Close when the URL changes (patches `pushState`/`replaceState` + popstate) |
|
|
245
|
-
| `lockBodyScroll` | `true` | iOS-safe body lock (`position: fixed`) |
|
|
246
|
-
| `stackEffect` | `false` | iOS card-stack: back sheets scale down per depth |
|
|
247
|
-
| `inertSiblings` | `false` | Mark page siblings `inert` while open (full modal) |
|
|
248
|
-
| `persistent` | `false` | Block dismissal (backdrop / Escape / back); programmatic `close()` still works |
|
|
249
|
-
| `disableClose` | `false` | Block all closing, including programmatic |
|
|
250
|
-
| `disableDrag` | `false` | Suppress the drag gesture (imperative snaps still work) |
|
|
251
|
-
| `dragFrom` | `"handle"` | Which regions start a drag: `"handle"`, `"sheet"` (whole sheet), or `"zones"` (only `[data-bs-drag]` subtrees). Defaults to `"sheet"` when no handle is given. `[data-bs-no-drag]` opts a subtree out in every mode. Also `setDragFrom()` / `getDragFrom()` — switching remounts the gesture, no component remount |
|
|
252
|
-
| `dragFromContent` | `true` | Whether a touch gesture on the scroll container drags the sheet; override per snap point with `{ id, size, dragFromContent: false }`. Also `setDragFromContent()` |
|
|
253
|
-
| `fitContentToSnap` | `false` | Keeps the whole scroll container reachable and the footer on screen at every snap point, not only the largest. Off by default; see [Content reachable at every snap](#content-reachable-at-every-snap) |
|
|
254
|
-
| `radius` | token default | Corner radius (`string` CSS length or `number` px); also `setRadius()` |
|
|
255
|
-
| `maxHeight` | none | Cap the sheet height. `number` (px) or a string (`"92dvh"`, `"50%"`) re-resolved on viewport / orientation changes |
|
|
256
|
-
| `returnFocusTo` | opener | Focus target on dismiss — an `HTMLElement`, a selector `string`, or a `() => HTMLElement` factory |
|
|
257
|
-
|
|
258
|
-
### Anchored elements, docked bars & scrim stages
|
|
259
|
-
|
|
260
|
-
Pin UI around the sheet — it rides the motion on the compositor (zero JS per
|
|
261
|
-
frame) and shows/hides per state with configurable animations:
|
|
200
|
+
</details>
|
|
262
201
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
element: closeBtn,
|
|
266
|
-
position: 'sheet-top-right', // rides the sheet edge; 9 screen positions too
|
|
267
|
-
showOn: ['half', 'full'], // snap ids or a predicate
|
|
268
|
-
animation: 'pop', // fade | scale | slide | pop | custom keyframes
|
|
269
|
-
});
|
|
202
|
+
<details>
|
|
203
|
+
<summary><b>Qwik</b></summary>
|
|
270
204
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
205
|
+
```tsx
|
|
206
|
+
import { component$, useSignal } from '@builder.io/qwik';
|
|
207
|
+
import { BottomSheet } from '@surdeddd/bottom-sheet/qwik';
|
|
208
|
+
import '@surdeddd/bottom-sheet/styles';
|
|
275
209
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
210
|
+
export const Filters = component$(() => {
|
|
211
|
+
const open = useSignal(false);
|
|
212
|
+
return (
|
|
213
|
+
<>
|
|
214
|
+
<button onClick$={() => (open.value = true)}>Filters</button>
|
|
215
|
+
<BottomSheet
|
|
216
|
+
open={open.value}
|
|
217
|
+
onOpenChange$={next => (open.value = next)}
|
|
218
|
+
snapPoints={[{ id: 'closed', size: 0 }, { id: 'open', size: 'fit' }]}
|
|
219
|
+
focusTrap
|
|
220
|
+
>
|
|
221
|
+
<h2 q:slot="header">Filters</h2>
|
|
222
|
+
<FilterForm />
|
|
223
|
+
</BottomSheet>
|
|
224
|
+
</>
|
|
225
|
+
);
|
|
282
226
|
});
|
|
283
227
|
```
|
|
284
228
|
|
|
285
|
-
[
|
|
229
|
+
[Qwik guide →](docs/qwik.md)
|
|
286
230
|
|
|
287
|
-
|
|
231
|
+
</details>
|
|
288
232
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
engine.on('open', ({ id }) => {}); // 0 → >0 (enter starts)
|
|
292
|
-
engine.on('opened', ({ id }) => {}); // enter settled
|
|
293
|
-
engine.on('close', () => {}); // → 0 (exit starts)
|
|
294
|
-
engine.on('closed', () => {}); // exit settled
|
|
295
|
-
engine.on('dragstart', ({ size }) => {});
|
|
296
|
-
engine.on('drag', ({ size, delta }) => {}); // ~60 fps — payload is pooled, don't retain
|
|
297
|
-
engine.on('dragend', ({ size, velocity }) => {});
|
|
298
|
-
engine.on('progress', ({ value, size }) => {}); // ~60 fps
|
|
299
|
-
|
|
300
|
-
// Cancelable — call cancel() synchronously to veto the transition:
|
|
301
|
-
engine.on('before-snap', ({ id, size, previousId, cancel }) => {});
|
|
302
|
-
engine.on('before-close', ({ reason, cancel }) => {}); // reason: programmatic | backdrop | escape | back
|
|
303
|
-
```
|
|
304
|
-
|
|
305
|
-
### Imperative
|
|
306
|
-
|
|
307
|
-
```ts
|
|
308
|
-
engine.snapTo('half');
|
|
309
|
-
engine.open(); // → first allowed snap with resolved size > 0
|
|
310
|
-
engine.close(reason?); // → first snap that resolves to size 0, else first allowed
|
|
311
|
-
engine.expand(); // → largest allowed snap
|
|
312
|
-
engine.collapse(); // → smallest allowed snap with size > 0
|
|
313
|
-
engine.setAllowed(ids, snapId); // dynamic allowlist
|
|
314
|
-
engine.setSnapPoints(points); // re-measure
|
|
315
|
-
engine.recompute(); // re-measure a 'fit' / 'content' snap on demand
|
|
316
|
-
engine.getResolvedSnaps(); // → [{ id, size }] resolved to pixels right now
|
|
317
|
-
engine.setPersistent(bool); // toggle persistence (block dismissal) at runtime
|
|
318
|
-
engine.setDisableClose(bool); // toggle close-blocking at runtime
|
|
319
|
-
engine.setDisableDrag(bool); // toggle the drag gesture at runtime
|
|
320
|
-
engine.setRadius('28px'); // corner radius
|
|
321
|
-
engine.setMaxHeight('92dvh'); // height cap (re-resolved on viewport change)
|
|
322
|
-
engine.canDismiss(); // false when persistent / disableClose
|
|
323
|
-
engine.isTop(); // top of the multi-sheet stack?
|
|
324
|
-
engine.depth(); // open sheets above this one
|
|
325
|
-
engine.destroy(); // remove listeners + cancel animation
|
|
326
|
-
```
|
|
233
|
+
<details>
|
|
234
|
+
<summary><b>Custom element</b> (Angular, Lit, Astro, plain HTML)</summary>
|
|
327
235
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
and teleported sheets. A sheet mounted hidden (`display:none` ancestor) heals
|
|
333
|
-
and emits its open sequence once on reveal. See
|
|
334
|
-
[Architecture → Opening at construction](docs/ARCHITECTURE.md).
|
|
335
|
-
|
|
336
|
-
### Scrim runtime API
|
|
236
|
+
```html
|
|
237
|
+
<script type="module">
|
|
238
|
+
import '@surdeddd/bottom-sheet/element';
|
|
239
|
+
</script>
|
|
337
240
|
|
|
338
|
-
|
|
339
|
-
scrim properties can be mutated at runtime — no remount needed.
|
|
241
|
+
<button onclick="document.querySelector('bottom-sheet').open()">Filters</button>
|
|
340
242
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
| `setScrimColor(color \ | null)` / `setScrimBlur(blur \ | null)` | Live color & backdrop-filter blur. `null` clears. |
|
|
347
|
-
| `setScrimInteractive(boolean)` | Toggle `pointer-events` on the scrim layer. | | |
|
|
348
|
-
| `setBackdropRange([s, e])` / `setScreenRange([s, e])` | Opacity-range mapping (progress 0..1 → opacity 0..1). | | |
|
|
349
|
-
| `setScrim({ ... })` | Batch — applies preset + individual fields in one pass with a single `applySize`. | | |
|
|
350
|
-
| `setScrimOverlay({ children, position, interactive })` | Inject a positioned floating element into the scrim area. Returns a teardown. Useful for monitoring-style "dim everything except this badge" UX. | | |
|
|
351
|
-
| `getScrimState()` | Read-only `{ mode, enabled }` snapshot. Stable public API for introspection without coupling to internals. | | |
|
|
243
|
+
<bottom-sheet snap-points="closed:0,open:fit" focus-trap="true">
|
|
244
|
+
<h2 slot="header">Filters</h2>
|
|
245
|
+
<form>…</form>
|
|
246
|
+
</bottom-sheet>
|
|
247
|
+
```
|
|
352
248
|
|
|
353
|
-
|
|
249
|
+
The element carries its own styles in its shadow root; no stylesheet is needed.
|
|
250
|
+
Without a bundler, load it from a CDN:
|
|
354
251
|
|
|
355
|
-
|
|
252
|
+
```html
|
|
253
|
+
<script src="https://unpkg.com/@surdeddd/bottom-sheet/dist/element.iife.global.js"></script>
|
|
254
|
+
```
|
|
356
255
|
|
|
357
|
-
|
|
358
|
-
panel that mirrors the scrim's runtime-mutability story for content.
|
|
256
|
+
[Custom element guide →](docs/web-component.md)
|
|
359
257
|
|
|
360
|
-
|
|
361
|
-
| ------------------------------- | ----------------------------------------------------------------------------------------------- | --------- | ---------------------------------------------------------------------------------------------------- |
|
|
362
|
-
| `setOverlayChildren(node \ | fragment \ | factory)` | Replace overlay contents at runtime. Pass a `Node`, a `DocumentFragment`, or a `() => Node` factory. |
|
|
363
|
-
| `clearOverlayChildren()` | Remove any injected content. | | |
|
|
364
|
-
| `setOverlay({ children, ... })` | Batch update — `children` is one of the accepted fields alongside positioning/preset overrides. | | |
|
|
258
|
+
</details>
|
|
365
259
|
|
|
366
|
-
|
|
367
|
-
|
|
260
|
+
<details>
|
|
261
|
+
<summary><b>No framework</b></summary>
|
|
368
262
|
|
|
369
|
-
|
|
263
|
+
```html
|
|
264
|
+
<div class="bs-root">
|
|
265
|
+
<div class="bs-backdrop"></div>
|
|
266
|
+
<section class="bs-sheet" data-mode="bottom" role="dialog" aria-label="Filters">
|
|
267
|
+
<div class="bs-handle" role="slider" tabindex="0" aria-label="Resize sheet"></div>
|
|
268
|
+
<div class="bs-content">…</div>
|
|
269
|
+
</section>
|
|
270
|
+
</div>
|
|
271
|
+
```
|
|
370
272
|
|
|
371
273
|
```ts
|
|
372
|
-
import {
|
|
274
|
+
import { BottomSheetEngine } from '@surdeddd/bottom-sheet';
|
|
275
|
+
import '@surdeddd/bottom-sheet/styles';
|
|
373
276
|
|
|
374
|
-
const
|
|
375
|
-
|
|
376
|
-
|
|
277
|
+
const root = document.querySelector('.bs-root')!;
|
|
278
|
+
const sheet = new BottomSheetEngine({
|
|
279
|
+
element: root.querySelector('.bs-sheet')!,
|
|
280
|
+
handle: root.querySelector('.bs-handle')!,
|
|
281
|
+
scrollContainer: root.querySelector('.bs-content')!,
|
|
282
|
+
backdrop: root.querySelector('.bs-backdrop')!,
|
|
283
|
+
snapPoints: [
|
|
284
|
+
{ id: 'closed', size: 0 },
|
|
285
|
+
{ id: 'open', size: 'fit' },
|
|
286
|
+
],
|
|
287
|
+
initial: 'closed',
|
|
377
288
|
});
|
|
378
289
|
|
|
379
|
-
|
|
380
|
-
sheets.transition(prev, next);
|
|
381
|
-
const cfg = sheets.resolve(currentRoute); // typed!
|
|
290
|
+
document.querySelector('#filters')!.addEventListener('click', () => sheet.open());
|
|
382
291
|
```
|
|
383
292
|
|
|
384
|
-
|
|
293
|
+
[Vanilla guide →](docs/vanilla.md)
|
|
385
294
|
|
|
386
|
-
|
|
295
|
+
</details>
|
|
387
296
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
240 Hz internally for stability, but the engine writes to the DOM exactly
|
|
393
|
-
once per `requestAnimationFrame`.
|
|
394
|
-
3. **Settled-state-only React snapshot.** `useSyncExternalStore` refreshes
|
|
395
|
-
only on `snap` / `dragstart` / `dragend`. Drag pixels and animation frames
|
|
396
|
-
propagate via CSS variables (`--bs-progress`, `--bs-size`) and direct
|
|
397
|
-
imperative subscriptions — no React re-renders on every frame.
|
|
297
|
+
Preact: `@surdeddd/bottom-sheet/preact` is the React adapter running on
|
|
298
|
+
`preact/compat` ([guide](docs/preact.md)). Angular, Lit and Astro use the
|
|
299
|
+
custom element or the engine: [Angular](docs/angular.md), [Lit](docs/lit.md),
|
|
300
|
+
[Astro](docs/astro.md).
|
|
398
301
|
|
|
399
|
-
##
|
|
302
|
+
## Concepts
|
|
400
303
|
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
~23 KB) and register just the features you use:
|
|
304
|
+
**Snap points** are the sizes the sheet can rest at. Each has an `id` you refer
|
|
305
|
+
to and a `size`:
|
|
404
306
|
|
|
405
307
|
```ts
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
features: [routeFeature(), contentSwipeFeature()],
|
|
413
|
-
});
|
|
308
|
+
type SnapPoint =
|
|
309
|
+
| number // pixels
|
|
310
|
+
| `${number}%` // share of the viewport along the sheet's axis
|
|
311
|
+
| 'fit' | 'content' // the sheet's own height: handle, header, content, footer
|
|
312
|
+
| 'full' // the whole axis
|
|
313
|
+
| string; // any CSS length: "50dvh", "clamp(200px, 60%, 800px)"
|
|
414
314
|
```
|
|
415
315
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
316
|
+
A point of size `0` is the **closed** state. A sheet without one cannot be
|
|
317
|
+
closed, only collapsed to its smallest point.
|
|
318
|
+
|
|
319
|
+
**Three layers** make up a sheet: the *sheet* itself, the *backdrop* that dims
|
|
320
|
+
the page, and an optional *scrim* (`screen`) for content that fades in behind
|
|
321
|
+
the sheet, such as a map preview or a blurred overlay.
|
|
322
|
+
|
|
323
|
+
**State** is one object: `{ activeId, size, progress, phase, isDragging,
|
|
324
|
+
isAnimating }`. `progress` runs from 0 at the smallest allowed point to 1 at the
|
|
325
|
+
largest. `phase` is `closed`, `opening`, `open` or `closing`.
|
|
326
|
+
|
|
327
|
+
## API
|
|
328
|
+
|
|
329
|
+
Every option, method and event below works the same in every adapter. Component
|
|
330
|
+
adapters take options as props (`focus-trap` in Vue templates, `focus-trap="true"`
|
|
331
|
+
on the custom element).
|
|
332
|
+
|
|
333
|
+
### Options
|
|
334
|
+
|
|
335
|
+
Sizing and position
|
|
336
|
+
|
|
337
|
+
| Option | Default | What it does |
|
|
338
|
+
| --- | --- | --- |
|
|
339
|
+
| `snapPoints` | required | List of `{ id, size }` |
|
|
340
|
+
| `allowed` | all ids | The points the sheet may rest at right now |
|
|
341
|
+
| `initial` | first allowed | The point to start at. A point larger than `0` starts the sheet open, with no `open` event |
|
|
342
|
+
| `mode` | `"bottom"` | Edge: `bottom`, `top`, `left`, `right`, or `start` / `end` |
|
|
343
|
+
| `maxHeight` | none | Cap on the sheet's size: pixels, or a CSS length resolved again when the viewport changes |
|
|
344
|
+
| `radius` | `20px` | Corner radius |
|
|
345
|
+
| `fitContentToSnap` | `false` | Keep the whole scroll area and the footer reachable at every point, not only the largest. [Details](docs/content-fit.md) |
|
|
346
|
+
|
|
347
|
+
Motion
|
|
348
|
+
|
|
349
|
+
| Option | Default | What it does |
|
|
350
|
+
| --- | --- | --- |
|
|
351
|
+
| `animation` | `"spring"` | `spring`, `ios-spring`, `material-bounce`, `snappy`, `tween`, `linear` |
|
|
352
|
+
| `spring` | preset | `{ stiffness, damping, mass }` |
|
|
353
|
+
| `duration`, `easing` | `220`, ease-out-back | Length in ms and curve `(t) => number` of the `tween` animation |
|
|
354
|
+
| `settleAnimation` | off | `"waapi"` runs the settle as a compositor-driven Web Animation |
|
|
355
|
+
| `respectReducedMotion` | `true` | Jump straight to the point under `prefers-reduced-motion: reduce` |
|
|
356
|
+
| `viewTransitions` | `false` | Move between points through `document.startViewTransition()` where the browser has it |
|
|
357
|
+
|
|
358
|
+
Gestures
|
|
359
|
+
|
|
360
|
+
| Option | Default | What it does |
|
|
361
|
+
| --- | --- | --- |
|
|
362
|
+
| `dragFrom` | `"handle"` | Where a drag may start: the `handle`, the whole `sheet`, or `zones` (`[data-bs-drag]` subtrees only). `[data-bs-no-drag]` opts a subtree out. A sheet without a handle defaults to `sheet` |
|
|
363
|
+
| `dragFromContent` | `true` | Whether a touch on the scroll area drags the sheet. Can be set per point: `{ id, size, dragFromContent: false }` |
|
|
364
|
+
| `disableDrag` | `false` | Turn the gesture off; methods still work |
|
|
365
|
+
| `rubberBand` | `true` | Soft over-drag past the last point |
|
|
366
|
+
| `flickVelocity` | `0.65` px/ms | Speed at which a touch or pen release counts as a flick. A mouse uses a fixed 0.4 |
|
|
367
|
+
| `dragThreshold` | `18` px | A touch or pen drag shorter than this snaps back. A mouse uses a fixed 12 |
|
|
368
|
+
|
|
369
|
+
Closing
|
|
370
|
+
|
|
371
|
+
| Option | Default | What it does |
|
|
372
|
+
| --- | --- | --- |
|
|
373
|
+
| `closeOnEscape` | `true` | Escape closes the top sheet |
|
|
374
|
+
| `closeOnBack` | `false` | The Back button closes the sheet instead of leaving the page |
|
|
375
|
+
| `routedTo` | none | URL shown while the sheet is open; Back closes it and restores the address |
|
|
376
|
+
| `closeOnRouteChange` | `false` | Close when the URL changes |
|
|
377
|
+
| `persistent` | `false` | The user cannot dismiss the sheet: no backdrop tap, Escape, Back, drag or key closes it. `close()` still does |
|
|
378
|
+
| `disableClose` | `false` | Nothing closes the sheet, `close()` included |
|
|
379
|
+
|
|
380
|
+
Focus and accessibility
|
|
381
|
+
|
|
382
|
+
| Option | Default | What it does |
|
|
383
|
+
| --- | --- | --- |
|
|
384
|
+
| `focusTrap` | `false` | Move focus into the sheet when it opens, keep Tab inside it while it is the top one, and give focus back when it closes. Mark a popup rendered elsewhere with `data-bs-focus-allow` to let focus go there |
|
|
385
|
+
| `initialFocus` | first focusable | With `focusTrap`: the element or selector that gets focus on open. The first focusable element is the handle; `false` focuses the sheet itself |
|
|
386
|
+
| `returnFocusTo` | none | Element, selector or function: where to send focus on close. A focus trap already returns it to where it was |
|
|
387
|
+
| `inertSiblings` | `false` | Make everything outside the sheet `inert` while it is the top one |
|
|
388
|
+
| `lockBodyScroll` | `true` | Lock page scroll while the sheet is open |
|
|
389
|
+
|
|
390
|
+
Backdrop and scrim
|
|
391
|
+
|
|
392
|
+
| Option | Default | What it does |
|
|
393
|
+
| --- | --- | --- |
|
|
394
|
+
| `backdropRange` | `[0, 1]` | The stretch of `progress` over which the backdrop fades in |
|
|
395
|
+
| `backdropOpacity` | `1` | The opacity the backdrop reaches at the end of that stretch |
|
|
396
|
+
| `backdropColor` | `rgb(0 0 0 / 0.5)` | Colour of the backdrop; the same as the `--bs-backdrop-color` token |
|
|
397
|
+
| `screenRange` | `[0, 1]` | The same stretch for the scrim |
|
|
398
|
+
| `scrimMode` | `"full"` | `full` screen, `above-sheet` (stops at the sheet's edge) or `off` |
|
|
399
|
+
| `scrimPreset` | none | `subtle`, `standard`, `monitoring`, `cinematic`: colour, blur and range in one |
|
|
400
|
+
| `scrimColor`, `scrimBlur` | preset | Scrim fill and the radius of its `backdrop-filter` blur |
|
|
401
|
+
| `scrimInteractive` | `false` | The scrim takes pointer input instead of letting it through |
|
|
402
|
+
| `scrimTapToClose` | `false` | A tap on the scrim closes the sheet |
|
|
403
|
+
|
|
404
|
+
More
|
|
405
|
+
|
|
406
|
+
| Option | Default | What it does |
|
|
407
|
+
| --- | --- | --- |
|
|
408
|
+
| `stackEffect` | `false` | Scale the sheets behind the top one, like the iOS card stack |
|
|
409
|
+
| `persistKey` | none | `localStorage` key: the sheet starts where it was left |
|
|
410
|
+
| `autoCollapseAfter` | off | Milliseconds of rest after which the sheet returns to its smallest open point |
|
|
411
|
+
| `linkedSheets` | none | Other engines that collapse when this one opens |
|
|
412
|
+
| `features` | all | Feature list for the [slim core](#slim-core) |
|
|
413
|
+
|
|
414
|
+
Component adapters add a few props of their own:
|
|
415
|
+
|
|
416
|
+
| Prop | Default | What it does |
|
|
417
|
+
| --- | --- | --- |
|
|
418
|
+
| `open`, `onOpenChange` | uncontrolled | Controlled open state. `v-model:open` in Vue, `bind:open` in Svelte, `onOpenChange$` in Qwik, the `open` attribute on the element |
|
|
419
|
+
| `snap` | uncontrolled | The active snap point, as a prop: `v-model:snap` in Vue, `bind:snap` in Svelte, `snap` with `onSnap$` in Qwik, the `snap` attribute on the element. React and Solid use the ref |
|
|
420
|
+
| `header`, `footer` | none | Regions above and below the scroll area (slots in Vue, snippets in Svelte) |
|
|
421
|
+
| `backdrop` | `true` | Render the backdrop |
|
|
422
|
+
| `closeOnBackdrop` | `true` | A tap on the backdrop closes the sheet |
|
|
423
|
+
| `teleportTo` | none | Move the sheet's DOM out of a clipping or transformed ancestor |
|
|
424
|
+
| `ariaLabel` | `"Bottom sheet"` | Accessible name of the sheet |
|
|
425
|
+
| `handleLabel`, `contentLabel` | English | Accessible names of the handle and the scroll area, for apps in another language |
|
|
426
|
+
|
|
427
|
+
### Methods
|
|
425
428
|
|
|
426
429
|
```ts
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
430
|
+
sheet.open(); // closed → the first allowed open point; an open sheet stays put
|
|
431
|
+
sheet.open('half'); // to a specific point
|
|
432
|
+
sheet.close(); // → the closed point; without one, the smallest point
|
|
433
|
+
sheet.snapTo('full');
|
|
434
|
+
sheet.expand(); // → the largest allowed point
|
|
435
|
+
sheet.collapse(); // → the smallest allowed open point
|
|
436
|
+
|
|
437
|
+
sheet.setAllowed(['half', 'full']); // change where it may rest
|
|
438
|
+
sheet.setSnapPoints(points); // replace the points
|
|
439
|
+
sheet.recompute(); // re-measure a 'fit' point
|
|
440
|
+
sheet.setMode('right');
|
|
441
|
+
sheet.setPersistent(true);
|
|
442
|
+
sheet.setDisableClose(true);
|
|
443
|
+
sheet.setDisableDrag(true);
|
|
444
|
+
sheet.setRadius('28px');
|
|
445
|
+
sheet.setMaxHeight('92dvh');
|
|
446
|
+
sheet.setBackdropOpacity(0.6);
|
|
447
|
+
sheet.setBackdropColor('rgb(0 0 0 / 0.4)');
|
|
448
|
+
|
|
449
|
+
sheet.state; // { activeId, size, progress, phase, isDragging, isAnimating }
|
|
450
|
+
sheet.getResolvedSnaps(); // [{ id, size }] in pixels right now
|
|
451
|
+
sheet.canDismiss(); // false while persistent or disableClose
|
|
452
|
+
sheet.isTop(); // is it the top sheet of the stack?
|
|
453
|
+
sheet.destroy();
|
|
433
454
|
```
|
|
434
455
|
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
sheet bends as one surface, and handed straight back to the DOM once it
|
|
438
|
-
settles. Layout, scrolling, gestures and the accessibility tree never leave the
|
|
439
|
-
DOM. 4.2 KB gzip, and only if you import it.
|
|
440
|
-
Falls back to the DOM renderer, silently and mid-session if it has to, when
|
|
441
|
-
there is no WebGL, under `prefers-reduced-motion`, or when the browser drops
|
|
442
|
-
the GL context.
|
|
456
|
+
`open()`, `close()` and `snapTo()` return a promise that resolves when the
|
|
457
|
+
sheet has landed. They are safe to call in the tick the sheet is mounted.
|
|
443
458
|
|
|
444
|
-
|
|
459
|
+
When `setAllowed()` or `setSnapPoints()` takes the active point away, an open
|
|
460
|
+
sheet moves to the nearest point that keeps it open.
|
|
445
461
|
|
|
446
|
-
|
|
462
|
+
In components these are on the ref (`ref.current.open()`), on the element
|
|
463
|
+
(`el.open()`), or on the object the composable returns. Anything missing there
|
|
464
|
+
is one call away through `getEngine()`.
|
|
447
465
|
|
|
448
|
-
|
|
449
|
-
import { attachDebugOverlay } from '@surdeddd/bottom-sheet/debug';
|
|
466
|
+
### Events
|
|
450
467
|
|
|
451
|
-
|
|
468
|
+
```ts
|
|
469
|
+
sheet.on('open', ({ id }) => {}); // the sheet starts to appear
|
|
470
|
+
sheet.on('opened', ({ id }) => {}); // it has landed
|
|
471
|
+
sheet.on('close', () => {}); // it starts to leave
|
|
472
|
+
sheet.on('closed', () => {}); // it is gone: unmount here
|
|
473
|
+
sheet.on('snap', ({ id, size, progress }) => {}); // it settled on a point
|
|
474
|
+
|
|
475
|
+
sheet.on('dragstart', ({ size }) => {});
|
|
476
|
+
sheet.on('drag', ({ size, delta }) => {}); // every frame
|
|
477
|
+
sheet.on('dragend', ({ size, velocity }) => {});
|
|
478
|
+
sheet.on('progress', ({ value, size }) => {}); // every frame of any motion
|
|
479
|
+
|
|
480
|
+
sheet.on('before-snap', ({ id, previousId, cancel }) => {});
|
|
481
|
+
sheet.on('before-close', ({ reason, cancel }) => {});
|
|
452
482
|
```
|
|
453
483
|
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
484
|
+
Call `cancel()` synchronously inside `before-snap` or `before-close` to stop
|
|
485
|
+
the move; `reason` is `programmatic`, `backdrop`, `escape` or `back`.
|
|
486
|
+
|
|
487
|
+
The scroll lock, the focus trap and the history entry go on at `open`. The
|
|
488
|
+
history entry comes off at `close`; the lock and the trap at `closed`, when
|
|
489
|
+
the sheet is gone. A move reversed half-way fires `open` or `close` again, so
|
|
490
|
+
the two always alternate.
|
|
491
|
+
|
|
492
|
+
Components expose these as `onSnap`, `onOpen`… props (React, Solid),
|
|
493
|
+
`@snap`, `@open`… (Vue), `onsnap`… (Svelte), `onSnap$`… (Qwik) and DOM events
|
|
494
|
+
on the custom element.
|
|
495
|
+
|
|
496
|
+
### The keyboard
|
|
457
497
|
|
|
458
|
-
|
|
498
|
+
The handle is a slider. The arrow that points away from the sheet's edge grows
|
|
499
|
+
it (`ArrowUp` for a bottom sheet, `ArrowLeft` for a right one), the opposite one
|
|
500
|
+
shrinks it, `Home` and `End` jump to the smallest and largest point, `Escape`
|
|
501
|
+
closes.
|
|
459
502
|
|
|
460
|
-
|
|
461
|
-
`dist/qwik-lib/index.qwik.mjs`. Qwik apps pick the adapter up automatically
|
|
462
|
-
as a vendor root: the consumer optimizer segments it into QRL chunks and
|
|
463
|
-
`renderToString` emits a resumable (`q:container="paused"`) sheet. No extra
|
|
464
|
-
configuration — `import { BottomSheet } from '@surdeddd/bottom-sheet/qwik'`
|
|
465
|
-
keeps working for CSR, and Qwik City SSR now works out of the box.
|
|
503
|
+
### More of the API
|
|
466
504
|
|
|
467
|
-
|
|
505
|
+
| Topic | Guide |
|
|
506
|
+
| --- | --- |
|
|
507
|
+
| Buttons and bars pinned to the sheet's edge, scrim content per state | [Anchors and scrim stages](docs/anchors.md) |
|
|
508
|
+
| Changing the scrim at runtime | [Theming → the scrim](docs/theming.md#the-scrim) |
|
|
509
|
+
| A lighter slide-up panel without snap points | [Overlay](docs/overlay.md) |
|
|
510
|
+
| Sheets configured per route | [Vanilla → sheet manager](docs/vanilla.md#sheet-manager) |
|
|
511
|
+
| Navigating right after a close with `closeOnBack` | [Vanilla → waiting for the history](docs/vanilla.md#waiting-for-the-history) |
|
|
512
|
+
| Moving the sheet from code, frame by frame | [Recipe: programmatic drag](docs/recipes/programmatic-drag.md) |
|
|
513
|
+
| Your own engine extensions | [Plugins](docs/plugins.md) |
|
|
514
|
+
| Right-to-left interfaces | [RTL](docs/rtl.md) |
|
|
468
515
|
|
|
469
|
-
|
|
516
|
+
## Styling
|
|
517
|
+
|
|
518
|
+
Everything visual is a CSS custom property. Set them on `.bs-root`, on a parent,
|
|
519
|
+
or on `:root`; they reach into the custom element's shadow root too.
|
|
470
520
|
|
|
471
521
|
```css
|
|
472
522
|
.bs-root {
|
|
473
523
|
--bs-surface: #18181b;
|
|
474
|
-
--bs-handle-color: #71717a;
|
|
475
524
|
--bs-radius: 28px;
|
|
476
|
-
--bs-
|
|
525
|
+
--bs-handle-color: #71717a;
|
|
526
|
+
--bs-backdrop-color: rgb(0 0 0 / 0.6);
|
|
477
527
|
}
|
|
478
528
|
```
|
|
479
529
|
|
|
480
|
-
The engine writes `--bs-progress` (0
|
|
481
|
-
|
|
530
|
+
The engine writes `--bs-progress` (0 to 1) and `--bs-size` (pixels) on the
|
|
531
|
+
sheet and on `.bs-root` on every frame, so anything inside the root, such as
|
|
532
|
+
the `screen` layer or a floating button, follows the sheet without JavaScript:
|
|
482
533
|
|
|
483
534
|
```css
|
|
484
|
-
.
|
|
485
|
-
|
|
486
|
-
}
|
|
487
|
-
.map-canvas {
|
|
488
|
-
filter: blur(calc(var(--bs-progress) * 8px));
|
|
489
|
-
}
|
|
490
|
-
.search-input {
|
|
491
|
-
opacity: calc(1 - var(--bs-progress));
|
|
492
|
-
}
|
|
535
|
+
.bs-root .map-preview { filter: blur(calc(var(--bs-progress) * 8px)); }
|
|
536
|
+
.bs-root .fab { bottom: calc(var(--bs-size) + 16px); }
|
|
493
537
|
```
|
|
494
538
|
|
|
495
|
-
|
|
496
|
-
> `.bs-sheet`, `.bs-backdrop` and anchor wrappers on every stack change (base
|
|
497
|
-
> `100`, step `10` per depth), so a value you set is overwritten and races the
|
|
498
|
-
> stack. To position the sheet layer against app UI, move the container
|
|
499
|
-
> (teleport target / `.bs-root` parent) or change the open order — don't style
|
|
500
|
-
> `z-index` on bs-managed elements. See
|
|
501
|
-
> [Architecture → z-index & the sheet stack](docs/ARCHITECTURE.md).
|
|
502
|
-
|
|
503
|
-
**Every sheet owns its backdrop.** Open a list, then a detail sheet over it,
|
|
504
|
-
and both backdrops stay live: the dimming deepens with each level, and the
|
|
505
|
-
sheet underneath stays masked for the whole time the one above it closes, so
|
|
506
|
-
taps never reach the page mid-animation. Each backdrop follows its own sheet's
|
|
507
|
-
progress, so a buried sheet that closes clears its own scrim. The stack keeps
|
|
508
|
-
the order sheets started opening in — a sheet opened while another is still
|
|
509
|
-
animating stays on top. If you want a single dim level, pass `backdrop={false}`
|
|
510
|
-
(`:backdrop="false"` in Vue) to the sheets you open on top.
|
|
511
|
-
|
|
512
|
-
### Content reachable at every snap
|
|
513
|
-
|
|
514
|
-
The sheet is always as tall as its largest snap point and is moved with a
|
|
515
|
-
transform; that is what keeps a drag on the compositor. The cost is that below
|
|
516
|
-
the largest snap the bottom of the scroll container sits under the screen edge,
|
|
517
|
-
by exactly `largest snap − current size`. At a half snap you can scroll, but the
|
|
518
|
-
last rows never come into view until the sheet is pulled up.
|
|
519
|
-
|
|
520
|
-
`fitContentToSnap` closes that gap:
|
|
539
|
+
Three ready-made looks ship as extra stylesheets:
|
|
521
540
|
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
<LongList />
|
|
525
|
-
</BottomSheet>
|
|
526
|
-
```
|
|
527
|
-
|
|
528
|
-
When the sheet settles, the engine writes the hidden amount to
|
|
529
|
-
`--bs-content-inset` and the stylesheet shrinks `.bs-content` by exactly that
|
|
530
|
-
much, so the scroll container ends at the edge of the screen: the last row, a
|
|
531
|
-
focused field and the scrollbar all stay in the visible part, and the thumb is
|
|
532
|
-
sized for what you can actually see. The moment the sheet starts to move, it
|
|
533
|
-
switches to the full-height container with a spacer of the same size at the end
|
|
534
|
-
of the list, so the rows below the edge are already laid out when a drag or an
|
|
535
|
-
animation reveals them. The switch changes the box and the scroll range by the
|
|
536
|
-
same amount, so nothing on screen moves. That is one layout when motion starts
|
|
537
|
-
and one when it settles, none per frame, so the motion stays transform-only.
|
|
538
|
-
While the sheet moves, a list that was scrolled to its very end is held in place
|
|
539
|
-
on screen instead of riding up with the sheet and dropping back at the end.
|
|
540
|
-
|
|
541
|
-
The footer has the same problem and gets the same cure. It is the last thing in
|
|
542
|
-
the sheet, so below the largest snap it sits under the screen edge along with
|
|
543
|
-
the end of the list. With `fitContentToSnap` on, the shrunken container puts the
|
|
544
|
-
footer at the bottom of the visible part of the sheet at rest, and while the
|
|
545
|
-
sheet moves the stylesheet lifts `.bs-footer` by `--bs-size − --bs-max-size`, so
|
|
546
|
-
it rides that edge on every frame of a drag or an animation, still without a
|
|
547
|
-
layout. The list ends above it, not under it. On a snap too short to hold both,
|
|
548
|
-
the footer slides under the screen edge instead of covering the handle. The
|
|
549
|
-
WebGL renderer gets the same layout; while the sheet moves its footer paints
|
|
550
|
-
the surface colour, and a drag lifts the list into a texture that follows the
|
|
551
|
-
list's scroll position, so a list held at its end stays put there as well.
|
|
552
|
-
|
|
553
|
-
#### Scrolling the list at every snap on touch
|
|
554
|
-
|
|
555
|
-
Making the end of the list *reachable* and letting a finger *scroll* to it are
|
|
556
|
-
two separate things. By default a swipe that starts on the content drags the
|
|
557
|
-
sheet until it reaches its largest snap, and only then scrolls the list — the
|
|
558
|
-
native sheet behaviour. On a mouse or trackpad the wheel scrolls at any snap
|
|
559
|
-
regardless; on touch it does not. To scroll at a half snap with a finger, hand
|
|
560
|
-
content swipes to the list and keep the handle (and header) for moving the
|
|
561
|
-
sheet:
|
|
562
|
-
|
|
563
|
-
```tsx
|
|
564
|
-
<BottomSheet snapPoints={points} fitContentToSnap dragFromContent={false}>
|
|
565
|
-
<LongList />
|
|
566
|
-
</BottomSheet>
|
|
541
|
+
```ts
|
|
542
|
+
import '@surdeddd/bottom-sheet/themes/ios'; // also /material, /vercel
|
|
567
543
|
```
|
|
568
544
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
```ts
|
|
573
|
-
const points = [
|
|
574
|
-
{ id: "peek", size: 120 },
|
|
575
|
-
{ id: "half", size: "50%", dragFromContent: false },
|
|
576
|
-
{ id: "full", size: "90%", dragFromContent: false },
|
|
577
|
-
];
|
|
545
|
+
```html
|
|
546
|
+
<div class="bs-theme-ios"><!-- the sheet --></div>
|
|
578
547
|
```
|
|
579
548
|
|
|
580
|
-
|
|
581
|
-
child of the sheet's flex column, the way `.bs-content` is. For any other
|
|
582
|
-
container, `data-bs-fit-content` on the sheet reads `rest` or `moving` and
|
|
583
|
-
`--bs-content-inset` holds the hidden amount: shrink the container by it at
|
|
584
|
-
rest, or pad the end of its content by it while moving. On the slim core add
|
|
585
|
-
`contentFitFeature()` from `@surdeddd/bottom-sheet/features`.
|
|
549
|
+
[All tokens, themes, stacking and the custom element's parts →](docs/theming.md)
|
|
586
550
|
|
|
587
|
-
|
|
551
|
+
## Accessibility
|
|
588
552
|
|
|
589
|
-
|
|
590
|
-
| ------------------------ | ----------- | ---------------------------------------------------- |
|
|
591
|
-
| `--bs-scrim-color` | transparent | Fill of the scrim layer (`.bs-screen`) |
|
|
592
|
-
| `--bs-scrim-blur` | `0px` | `backdrop-filter: blur()` applied to the scrim layer |
|
|
593
|
-
| `--bs-header-min-height` | `0px` | Minimum height of the `.bs-header` region |
|
|
553
|
+
What is built in, and what you switch on:
|
|
594
554
|
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
555
|
+
- **The handle is a slider.** It has a label, a current value and arrow-key
|
|
556
|
+
steps, and it is in the tab order. Name it in your language with
|
|
557
|
+
`handleLabel`; `contentLabel` does the same for the scroll area.
|
|
558
|
+
- **The sheet has a name.** `ariaLabel`, or `ariaLabelledBy` pointing at a
|
|
559
|
+
heading. With `focusTrap` it is a `dialog` with `aria-modal`.
|
|
560
|
+
- **Focus.** `focusTrap` moves focus into the sheet, keeps Tab inside it and
|
|
561
|
+
returns focus to the opener on close. In a stack, only the top sheet traps.
|
|
562
|
+
- **The page behind.** `inertSiblings` takes everything outside the top sheet
|
|
563
|
+
out of reach of the keyboard and of screen readers.
|
|
564
|
+
- **Announcements.** Each settle is announced through a polite live region.
|
|
565
|
+
- **Motion and contrast.** `prefers-reduced-motion` turns animations into
|
|
566
|
+
jumps, and `forced-colors` gets a border, a visible handle and a system
|
|
567
|
+
focus ring.
|
|
600
568
|
|
|
601
|
-
|
|
569
|
+
A modal sheet should have `focusTrap` and `inertSiblings` both on.
|
|
602
570
|
|
|
603
|
-
|
|
604
|
-
Defaults match the built-in look. Set the variable on the host / `.bs-root` /
|
|
605
|
-
`:root`; custom properties inherit through the Web Component's shadow boundary,
|
|
606
|
-
so the same override works for every adapter:
|
|
571
|
+
## Forms and the on-screen keyboard
|
|
607
572
|
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
| `--bs-header-padding` | `0 16px 8px` | header slot |
|
|
613
|
-
| `--bs-footer-padding` | `12px 16px (+ inset)` | footer slot |
|
|
573
|
+
A bottom sheet rests on top of the phone keyboard, not under it, so a field
|
|
574
|
+
being typed in stays visible. With `focusTrap`, focus goes to the handle when
|
|
575
|
+
the sheet opens, so the keyboard stays down until the user taps a field;
|
|
576
|
+
`initialFocus="#email"` starts in a field instead.
|
|
614
577
|
|
|
615
|
-
|
|
616
|
-
keeps action buttons clear of the home indicator.)
|
|
578
|
+
To stop a sheet with unsaved input from closing, veto the move:
|
|
617
579
|
|
|
618
|
-
```
|
|
619
|
-
.
|
|
620
|
-
.
|
|
621
|
-
|
|
580
|
+
```ts
|
|
581
|
+
sheet.on('before-close', ({ cancel }) => {
|
|
582
|
+
if (form.isDirty) cancel();
|
|
583
|
+
});
|
|
622
584
|
```
|
|
623
585
|
|
|
624
|
-
|
|
625
|
-
a `'content'` / `'fit'` snap taller than the cap settles flush against the edge
|
|
626
|
-
and the body scrolls internally — the sheet never lifts off its anchor and
|
|
627
|
-
`--bs-size` always equals the rendered height.
|
|
586
|
+
For React Hook Form and Formik this is one line:
|
|
628
587
|
|
|
629
|
-
|
|
588
|
+
```ts
|
|
589
|
+
import { useFormGuard } from '@surdeddd/bottom-sheet/integrations/react-hook-form';
|
|
630
590
|
|
|
631
|
-
|
|
632
|
-
npm run demo
|
|
633
|
-
# http://localhost:5173
|
|
591
|
+
useFormGuard(sheetRef, form); // stays put while submitting, stays open while dirty
|
|
634
592
|
```
|
|
635
593
|
|
|
636
|
-
|
|
637
|
-
(`bottom · top · left · right · overlay`), and live engine readouts (active
|
|
638
|
-
snap, progress, velocity, FPS). EN/RU and light/dark toggles in the corner.
|
|
594
|
+
## Server rendering
|
|
639
595
|
|
|
640
|
-
|
|
596
|
+
Every adapter can be imported and rendered on the server; the engine attaches
|
|
597
|
+
on the client. A sheet that starts closed is hidden in the server markup, so
|
|
598
|
+
nothing flashes before hydration. This covers Next.js, Nuxt, SvelteKit,
|
|
599
|
+
SolidStart, Qwik City and Astro; the per-framework guides have the details,
|
|
600
|
+
such as `"use client"` in the Next.js App Router.
|
|
641
601
|
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
602
|
+
## Slim core
|
|
603
|
+
|
|
604
|
+
The default engine includes every optional behaviour. If size matters more,
|
|
605
|
+
start from the slim core and add the features you use:
|
|
646
606
|
|
|
647
607
|
```ts
|
|
648
|
-
|
|
608
|
+
import { BottomSheetCore } from '@surdeddd/bottom-sheet/core';
|
|
609
|
+
import { routeFeature, contentSwipeFeature } from '@surdeddd/bottom-sheet/features';
|
|
610
|
+
|
|
611
|
+
const sheet = new BottomSheetCore({
|
|
612
|
+
element, handle, snapPoints,
|
|
613
|
+
closeOnBack: true,
|
|
614
|
+
features: [routeFeature(), contentSwipeFeature()],
|
|
615
|
+
});
|
|
649
616
|
```
|
|
650
617
|
|
|
651
|
-
|
|
618
|
+
| Entry | gzip | What is in it |
|
|
619
|
+
| --- | --- | --- |
|
|
620
|
+
| `@surdeddd/bottom-sheet` | 25.6 KB | The full engine |
|
|
621
|
+
| `/core` | 21.9 KB | Gestures, snap points, focus, scroll lock, scrim, stacking |
|
|
622
|
+
| `/features` | 4.2 KB | `routeFeature`, `visualViewportFeature`, `contentSwipeFeature`, `contentFitFeature`, `persistFeature`, `autoCollapseFeature` |
|
|
623
|
+
| `/react` `/vue` `/svelte` `/solid` `/qwik` | 27 to 33 KB | The full engine plus the adapter |
|
|
624
|
+
| `/element` | 33.3 KB | The full engine, the element and its styles |
|
|
625
|
+
| `/overlay` | 8.4 KB | The standalone [overlay](docs/overlay.md) |
|
|
626
|
+
| `/webgl` | 5.9 KB | Optional [GPU renderer](docs/webgl.md) for the sheet surface |
|
|
627
|
+
| `/debug` | 0.9 KB | `attachDebugOverlay(engine)`: a live panel of the engine's state |
|
|
628
|
+
|
|
629
|
+
An option whose feature is missing warns once in development and is ignored.
|
|
652
630
|
|
|
653
631
|
## Browser support
|
|
654
632
|
|
|
655
|
-
|
|
656
|
-
ES2020 build
|
|
657
|
-
Events, CSS custom properties, `ResizeObserver
|
|
658
|
-
|
|
633
|
+
Chrome and Edge 80, Safari 13.1 (iOS 13.4), Firefox 72. That floor comes from
|
|
634
|
+
the ES2020 build and from the APIs the engine cannot do without: Pointer
|
|
635
|
+
Events, CSS custom properties, `ResizeObserver`. Everything else, from `inert`
|
|
636
|
+
to `backdrop-filter` to WebGL, is detected and degrades.
|
|
637
|
+
[Support matrix →](docs/browser-support.md)
|
|
659
638
|
|
|
660
|
-
|
|
639
|
+
## Documentation
|
|
661
640
|
|
|
662
|
-
|
|
641
|
+
| | |
|
|
642
|
+
| --- | --- |
|
|
643
|
+
| Frameworks | [React](docs/react.md) · [Vue](docs/vue.md) · [Svelte](docs/svelte.md) · [Solid](docs/solid.md) · [Qwik](docs/qwik.md) · [Preact](docs/preact.md) · [Custom element](docs/web-component.md) · [Vanilla](docs/vanilla.md) · [Angular](docs/angular.md) · [Lit](docs/lit.md) · [Astro](docs/astro.md) |
|
|
644
|
+
| Topics | [Theming](docs/theming.md) · [Anchors and scrim stages](docs/anchors.md) · [Content at every snap](docs/content-fit.md) · [Overlay](docs/overlay.md) · [RTL](docs/rtl.md) · [WebGL renderer](docs/webgl.md) · [Plugins](docs/plugins.md) |
|
|
645
|
+
| Recipes | [Ten worked examples](docs/recipes/README.md): filter panel, checkout, comment thread, navigation drawer, mini player and more |
|
|
646
|
+
| Examples | [Runnable projects](https://github.com/Surdeddd/BottomSheet/tree/main/examples) for React, Vue, Svelte, Solid, Lit, the custom element and vanilla |
|
|
647
|
+
| Coming from another library | [From vaul or react-modal-sheet](docs/MIGRATION.md) |
|
|
648
|
+
| Project | [Architecture](docs/ARCHITECTURE.md) · [API stability](docs/api-stability.md) · [Browser support](docs/browser-support.md) · [Contributing](CONTRIBUTING.md) |
|
|
663
649
|
|
|
664
|
-
|
|
665
|
-
npm test # 500+ unit tests via vitest + happy-dom
|
|
666
|
-
npx playwright test # e2e via Playwright on mobile-Chrome / mobile-Safari / Firefox
|
|
667
|
-
npm run typecheck # TypeScript --noEmit
|
|
668
|
-
```
|
|
650
|
+
## For AI coding assistants
|
|
669
651
|
|
|
670
|
-
|
|
652
|
+
[`llms.txt`](llms.txt) is a compact, complete description of the API written to
|
|
653
|
+
be read by a model: entry points, every option, the method and event names, and
|
|
654
|
+
the mistakes to avoid. It ships inside the npm package, next to this README.
|
|
671
655
|
|
|
672
656
|
## Contributing
|
|
673
657
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
658
|
+
```bash
|
|
659
|
+
npm install
|
|
660
|
+
npm run demo # the demo site on localhost:5173
|
|
661
|
+
npm test # unit tests
|
|
662
|
+
npm run e2e # browser tests
|
|
663
|
+
```
|
|
664
|
+
|
|
665
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). Bugs and ideas go to
|
|
666
|
+
[issues](https://github.com/Surdeddd/BottomSheet/issues).
|
|
679
667
|
|
|
680
668
|
## License
|
|
681
669
|
|
|
682
|
-
MIT
|
|
670
|
+
MIT, see [LICENSE](LICENSE).
|