@surdeddd/bottom-sheet 0.23.4 → 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 -527
- package/README.ru.md +533 -545
- package/dist/{BottomSheetCore-C5tJYVMY.d.cts → BottomSheetCore-B_r4KCQL.d.cts} +48 -13
- package/dist/{BottomSheetCore-DxSrC4QC.d.ts → BottomSheetCore-B_r4KCQL.d.ts} +48 -13
- package/dist/{BottomSheetEngine-B3F9qukX.d.cts → BottomSheetEngine-0F7mjyvF.d.cts} +1 -1
- package/dist/{BottomSheetEngine-BchzgqGG.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 +0 -1
- package/dist/debug.d.ts +0 -1
- package/dist/{default-features-DwVTzDwP.d.ts → default-features-4Xwv6aVt.d.ts} +4 -2
- package/dist/{default-features-BGiCSmV1.d.cts → default-features-DX2NOxP6.d.cts} +4 -2
- package/dist/element.cjs +1421 -697
- package/dist/element.d.cts +11 -3
- package/dist/element.d.ts +11 -3
- package/dist/element.iife.global.js +2 -2
- package/dist/element.js +1421 -697
- 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-C7TSQkVK.d.cts → index-2shlxhcG.d.cts} +1 -1
- package/dist/{index-CdM1tke5.d.ts → index-CNCRQnpc.d.ts} +1 -1
- package/dist/index.cjs +1129 -631
- package/dist/index.d.cts +11 -6
- package/dist/index.d.ts +11 -6
- package/dist/index.js +1129 -632
- package/dist/integrations/formik.cjs +21 -10
- package/dist/integrations/formik.d.cts +0 -1
- package/dist/integrations/formik.d.ts +0 -1
- package/dist/integrations/formik.js +22 -11
- package/dist/integrations/react-hook-form.cjs +21 -12
- package/dist/integrations/react-hook-form.d.cts +0 -1
- package/dist/integrations/react-hook-form.d.ts +0 -1
- 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 +1 -3
- package/dist/overlay.d.ts +1 -3
- 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 +12 -30
- package/dist/qwik.d.ts +12 -30
- package/dist/qwik.js +1326 -701
- package/dist/react.cjs +1373 -750
- package/dist/react.d.cts +26 -12
- package/dist/react.d.ts +26 -12
- package/dist/react.js +1375 -752
- package/dist/solid.cjs +1332 -727
- package/dist/solid.d.cts +13 -438
- package/dist/solid.d.ts +13 -438
- package/dist/solid.js +1334 -729
- package/dist/solid.source.jsx +1374 -751
- package/dist/styles.css +20 -6
- package/dist/svelte-core.cjs +1209 -676
- package/dist/svelte-core.js +1207 -677
- package/dist/svelte-src/BottomSheet.svelte +101 -58
- package/dist/svelte.cjs +1323 -705
- package/dist/svelte.d.cts +45 -25
- package/dist/svelte.d.ts +45 -25
- package/dist/svelte.js +1323 -705
- 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 +0 -1
- package/dist/webgl.d.ts +0 -1
- package/dist/webgl.js +38 -7
- package/llms.txt +330 -0
- package/package.json +41 -19
- package/dist/transform-jVfAikE9.d.cts +0 -3
- package/dist/transform-jVfAikE9.d.ts +0 -3
package/README.md
CHANGED
|
@@ -1,59 +1,51 @@
|
|
|
1
1
|
# @surdeddd/bottom-sheet
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
](https://www.npmjs.com/package/@surdeddd/bottom-sheet)
|
|
10
|
+
[](https://www.npmjs.com/package/@surdeddd/bottom-sheet)
|
|
11
|
+

|
|
12
|
+

|
|
13
|
+

|
|
11
14
|

|
|
12
15
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
## 🚀 [Live demo → bottom-sheet-demo-ashy.vercel.app](https://bottom-sheet-demo-ashy.vercel.app)
|
|
18
|
-
|
|
19
|
-
📚 [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)
|
|
20
20
|
|
|
21
21
|
<p align="center">
|
|
22
22
|
<picture>
|
|
23
23
|
<source media="(prefers-color-scheme: dark)" srcset="docs/screenshots/sheet-dark.png" />
|
|
24
|
-
<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" />
|
|
25
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" />
|
|
26
27
|
</p>
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- **
|
|
45
|
-
|
|
46
|
-
- **
|
|
47
|
-
|
|
48
|
-
indicator.
|
|
49
|
-
- **WCAG 2.1 AA keyboard slider** — Arrow/Home/End/Esc stepping, focus trap
|
|
50
|
-
with restore.
|
|
51
|
-
- **Multi-sheet stacking** plus a typed sheet-manager registry.
|
|
52
|
-
- **Cancelable `before-snap` / `before-close`** — veto a transition
|
|
53
|
-
synchronously.
|
|
54
|
-
- Backed by 500+ unit tests across the suite and ~200 Playwright e2e runs
|
|
55
|
-
across three browser engines (`mobile-chrome` / `mobile-safari` /
|
|
56
|
-
`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.
|
|
57
49
|
|
|
58
50
|
## Install
|
|
59
51
|
|
|
@@ -61,624 +53,618 @@ Every item below is verifiable in this repo's `src/`:
|
|
|
61
53
|
npm i @surdeddd/bottom-sheet
|
|
62
54
|
```
|
|
63
55
|
|
|
64
|
-
|
|
65
|
-
the framework you use, the bundle pays only for that adapter.
|
|
56
|
+
Frameworks are optional peer dependencies: you install only the one you use.
|
|
66
57
|
|
|
67
58
|
## Quick start
|
|
68
59
|
|
|
69
|
-
###
|
|
60
|
+
### A modal sheet opened by a button
|
|
70
61
|
|
|
71
62
|
```tsx
|
|
63
|
+
import { useState } from 'react';
|
|
72
64
|
import { BottomSheet } from '@surdeddd/bottom-sheet/react';
|
|
73
65
|
import '@surdeddd/bottom-sheet/styles';
|
|
74
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
|
|
75
100
|
<BottomSheet
|
|
76
101
|
snapPoints={[
|
|
77
|
-
{ id: '
|
|
102
|
+
{ id: 'peek', size: 96 },
|
|
78
103
|
{ id: 'half', size: '45dvh' },
|
|
79
|
-
{ id: 'full', size: '
|
|
104
|
+
{ id: 'full', size: '90%' },
|
|
80
105
|
]}
|
|
81
|
-
initial=
|
|
82
|
-
|
|
83
|
-
spring={{ stiffness: 260, damping: 28 }}
|
|
84
|
-
focusTrap
|
|
85
|
-
closeOnEscape
|
|
86
|
-
header={<h2>Search vehicles</h2>}
|
|
106
|
+
initial="peek"
|
|
107
|
+
header={<h2>Nearby</h2>}
|
|
87
108
|
>
|
|
88
|
-
<
|
|
89
|
-
</BottomSheet
|
|
109
|
+
<PlaceList />
|
|
110
|
+
</BottomSheet>
|
|
90
111
|
```
|
|
91
112
|
|
|
92
|
-
|
|
113
|
+
### Other frameworks
|
|
93
114
|
|
|
94
|
-
|
|
115
|
+
<details>
|
|
116
|
+
<summary><b>Vue 3</b></summary>
|
|
95
117
|
|
|
96
118
|
```vue
|
|
97
|
-
<script setup>
|
|
98
|
-
|
|
99
|
-
|
|
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
|
+
];
|
|
100
129
|
</script>
|
|
101
130
|
|
|
102
131
|
<template>
|
|
103
|
-
<
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
@snap="onSnap"
|
|
108
|
-
>
|
|
109
|
-
<template #header><h2>Search</h2></template>
|
|
110
|
-
<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 />
|
|
111
136
|
</BottomSheet>
|
|
112
137
|
</template>
|
|
113
138
|
```
|
|
114
139
|
|
|
115
|
-
[
|
|
140
|
+
[Vue guide →](docs/vue.md)
|
|
141
|
+
|
|
142
|
+
</details>
|
|
116
143
|
|
|
117
|
-
|
|
144
|
+
<details>
|
|
145
|
+
<summary><b>Svelte 5</b></summary>
|
|
118
146
|
|
|
119
147
|
```svelte
|
|
120
148
|
<script lang="ts">
|
|
121
|
-
import {
|
|
122
|
-
import
|
|
123
|
-
|
|
124
|
-
let sheetEl: HTMLElement | undefined = $state();
|
|
125
|
-
let handleEl: HTMLElement | undefined = $state();
|
|
126
|
-
let contentEl: HTMLElement | undefined = $state();
|
|
127
|
-
|
|
128
|
-
const ctrl = createBottomSheet({
|
|
129
|
-
snapPoints: [{ id: "min", size: 96 }, { id: "full", size: "85%" }],
|
|
130
|
-
initial: "min",
|
|
131
|
-
});
|
|
132
|
-
|
|
133
|
-
$effect(() => {
|
|
134
|
-
if (!sheetEl) return;
|
|
135
|
-
return ctrl.attach({ element: sheetEl, handle: handleEl, scrollContainer: contentEl });
|
|
136
|
-
});
|
|
137
|
-
</script>
|
|
138
|
-
|
|
139
|
-
<section class="bs-sheet" bind:this={sheetEl} data-mode="bottom">
|
|
140
|
-
<div class="bs-handle" bind:this={handleEl}><h2>Search</h2></div>
|
|
141
|
-
<div class="bs-content" bind:this={contentEl}><!-- list --></div>
|
|
142
|
-
</section>
|
|
143
|
-
```
|
|
149
|
+
import { BottomSheet } from '@surdeddd/bottom-sheet/svelte';
|
|
150
|
+
import '@surdeddd/bottom-sheet/styles';
|
|
144
151
|
|
|
145
|
-
|
|
152
|
+
let open = $state(false);
|
|
153
|
+
</script>
|
|
146
154
|
|
|
147
|
-
|
|
155
|
+
<button onclick={() => (open = true)}>Filters</button>
|
|
148
156
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
<bottom-sheet
|
|
154
|
-
snap-points='[{"id":"min","size":96},{"id":"full","size":"85%"}]'
|
|
155
|
-
initial="min"
|
|
156
|
-
animation="spring"
|
|
157
|
-
focus-trap="true"
|
|
157
|
+
<BottomSheet
|
|
158
|
+
bind:open
|
|
159
|
+
snapPoints={[{ id: 'closed', size: 0 }, { id: 'open', size: 'fit' }]}
|
|
160
|
+
focusTrap
|
|
158
161
|
>
|
|
159
|
-
<h2
|
|
160
|
-
<
|
|
161
|
-
|
|
162
|
-
</ul>
|
|
163
|
-
</bottom-sheet>
|
|
162
|
+
{#snippet header()}<h2>Filters</h2>{/snippet}
|
|
163
|
+
{#snippet children()}<FilterForm />{/snippet}
|
|
164
|
+
</BottomSheet>
|
|
164
165
|
```
|
|
165
166
|
|
|
166
|
-
[
|
|
167
|
+
[Svelte guide →](docs/svelte.md)
|
|
167
168
|
|
|
168
|
-
|
|
169
|
+
</details>
|
|
169
170
|
|
|
170
|
-
|
|
171
|
-
|
|
171
|
+
<details>
|
|
172
|
+
<summary><b>Solid</b></summary>
|
|
172
173
|
|
|
173
|
-
```
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
rel="stylesheet"
|
|
177
|
-
href="https://unpkg.com/@surdeddd/bottom-sheet/dist/styles.css"
|
|
178
|
-
/>
|
|
179
|
-
<script src="https://unpkg.com/@surdeddd/bottom-sheet/dist/element.iife.global.js"></script>
|
|
180
|
-
<bottom-sheet
|
|
181
|
-
snap-points='[{"id":"min","size":96},{"id":"full","size":"85%"}]'
|
|
182
|
-
initial="min"
|
|
183
|
-
>
|
|
184
|
-
<h2 slot="header">Title</h2>
|
|
185
|
-
<p>Content</p>
|
|
186
|
-
</bottom-sheet>
|
|
187
|
-
```
|
|
188
|
-
|
|
189
|
-
### Vanilla core (no framework)
|
|
190
|
-
|
|
191
|
-
```ts
|
|
192
|
-
import { BottomSheetEngine } from '@surdeddd/bottom-sheet';
|
|
174
|
+
```tsx
|
|
175
|
+
import { createSignal } from 'solid-js';
|
|
176
|
+
import { BottomSheet } from '@surdeddd/bottom-sheet/solid';
|
|
193
177
|
import '@surdeddd/bottom-sheet/styles';
|
|
194
178
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
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
|
+
}
|
|
207
196
|
```
|
|
208
197
|
|
|
209
|
-
[
|
|
210
|
-
|
|
211
|
-
## API at a glance
|
|
198
|
+
[Solid guide →](docs/solid.md)
|
|
212
199
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
```ts
|
|
216
|
-
type SnapPoint =
|
|
217
|
-
| number // pixels
|
|
218
|
-
| `${number}%` // percent of viewport along the axis
|
|
219
|
-
| 'fit' // natural height of the whole sheet (handle + header + content + footer)
|
|
220
|
-
| 'content' // alias of 'fit'
|
|
221
|
-
| 'full' // 100 % of axis
|
|
222
|
-
| string; // any CSS length — "50dvh", "clamp(200px, 60%, 800px)"
|
|
223
|
-
```
|
|
200
|
+
</details>
|
|
224
201
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
| Option | Default | What it does |
|
|
228
|
-
| -------------------- | ------------- | -------------------------------------------------------------------------------------------------------------------------- |
|
|
229
|
-
| `snapPoints` | required | Ordered list of `{ id, size }` |
|
|
230
|
-
| `allowed` | all ids | Subset the sheet may settle on right now |
|
|
231
|
-
| `initial` | first allowed | Snap id to start at |
|
|
232
|
-
| `mode` | `"bottom"` | `bottom`, `top`, `left`, `right`, or logical `start` / `end`; reactive in React and Vue adapters |
|
|
233
|
-
| `animation` | `"spring"` | `spring`, `tween`, `ios-spring`, `material-bounce`, `linear`, `snappy` |
|
|
234
|
-
| `settleAnimation` | off | `"waapi"` — settle runs as a compositor-driven Web Animation (pre-sampled spring keyframes); falls back to rAF automatically |
|
|
235
|
-
| `features` | all defaults | `EngineFeature[]` — override/extend the composable feature set (see Slim core below) |
|
|
236
|
-
| `spring` | snappy | `{ stiffness, damping, mass }` |
|
|
237
|
-
| `flickVelocity` | `0.65 px/ms` | Mobile flick threshold |
|
|
238
|
-
| `dragThreshold` | `18 px` | Below this, drag snaps back |
|
|
239
|
-
| `rubberBand` | `true` | iOS-style soft over-drag |
|
|
240
|
-
| `backdropRange` | `[0, 1]` | Progress range over which backdrop fades |
|
|
241
|
-
| `screenRange` | `[0, 1]` | Progress range over which the `screen`/scrim content fades |
|
|
242
|
-
| `focusTrap` | `false` | Trap Tab focus inside the sheet when open |
|
|
243
|
-
| `initialFocus` | first field | Element / selector to focus on open; `false` focuses the sheet container (`tabindex=-1`) instead — avoids the iOS keyboard |
|
|
244
|
-
| `closeOnEscape` | `true` | Listen for Escape and close |
|
|
245
|
-
| `closeOnBack` | `false` | Intercept Android back button (history.popstate) |
|
|
246
|
-
| `closeOnRouteChange` | `false` | Close when the URL changes (patches `pushState`/`replaceState` + popstate) |
|
|
247
|
-
| `lockBodyScroll` | `true` | iOS-safe body lock (`position: fixed`) |
|
|
248
|
-
| `stackEffect` | `false` | iOS card-stack: back sheets scale down per depth |
|
|
249
|
-
| `inertSiblings` | `false` | Mark page siblings `inert` while open (full modal) |
|
|
250
|
-
| `persistent` | `false` | Block dismissal (backdrop / Escape / back); programmatic `close()` still works |
|
|
251
|
-
| `disableClose` | `false` | Block all closing, including programmatic |
|
|
252
|
-
| `disableDrag` | `false` | Suppress the drag gesture (imperative snaps still work) |
|
|
253
|
-
| `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 |
|
|
254
|
-
| `dragFromContent` | `true` | Whether a touch gesture on the scroll container drags the sheet; override per snap point with `{ id, size, dragFromContent: false }`. Also `setDragFromContent()` |
|
|
255
|
-
| `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) |
|
|
256
|
-
| `radius` | token default | Corner radius (`string` CSS length or `number` px); also `setRadius()` |
|
|
257
|
-
| `maxHeight` | none | Cap the sheet height. `number` (px) or a string (`"92dvh"`, `"50%"`) re-resolved on viewport / orientation changes |
|
|
258
|
-
| `returnFocusTo` | opener | Focus target on dismiss — an `HTMLElement`, a selector `string`, or a `() => HTMLElement` factory |
|
|
259
|
-
|
|
260
|
-
### Anchored elements, docked bars & scrim stages
|
|
261
|
-
|
|
262
|
-
Pin UI around the sheet — it rides the motion on the compositor (zero JS per
|
|
263
|
-
frame) and shows/hides per state with configurable animations:
|
|
202
|
+
<details>
|
|
203
|
+
<summary><b>Qwik</b></summary>
|
|
264
204
|
|
|
265
|
-
```
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
showOn: ['half', 'full'], // snap ids or a predicate
|
|
270
|
-
animation: 'pop', // fade | scale | slide | pop | custom keyframes
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
engine.addAnchor({
|
|
274
|
-
element: tabBar,
|
|
275
|
-
position: 'dock-bottom', // full-width bar; the sheet slides under it
|
|
276
|
-
});
|
|
205
|
+
```tsx
|
|
206
|
+
import { component$, useSignal } from '@builder.io/qwik';
|
|
207
|
+
import { BottomSheet } from '@surdeddd/bottom-sheet/qwik';
|
|
208
|
+
import '@surdeddd/bottom-sheet/styles';
|
|
277
209
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
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
|
+
);
|
|
284
226
|
});
|
|
285
227
|
```
|
|
286
228
|
|
|
287
|
-
[
|
|
229
|
+
[Qwik guide →](docs/qwik.md)
|
|
288
230
|
|
|
289
|
-
|
|
231
|
+
</details>
|
|
290
232
|
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
engine.on('open', ({ id }) => {}); // 0 → >0 (enter starts)
|
|
294
|
-
engine.on('opened', ({ id }) => {}); // enter settled
|
|
295
|
-
engine.on('close', () => {}); // → 0 (exit starts)
|
|
296
|
-
engine.on('closed', () => {}); // exit settled
|
|
297
|
-
engine.on('dragstart', ({ size }) => {});
|
|
298
|
-
engine.on('drag', ({ size, delta }) => {}); // ~60 fps — payload is pooled, don't retain
|
|
299
|
-
engine.on('dragend', ({ size, velocity }) => {});
|
|
300
|
-
engine.on('progress', ({ value, size }) => {}); // ~60 fps
|
|
301
|
-
|
|
302
|
-
// Cancelable — call cancel() synchronously to veto the transition:
|
|
303
|
-
engine.on('before-snap', ({ id, size, previousId, cancel }) => {});
|
|
304
|
-
engine.on('before-close', ({ reason, cancel }) => {}); // reason: programmatic | backdrop | escape | back
|
|
305
|
-
```
|
|
306
|
-
|
|
307
|
-
### Imperative
|
|
308
|
-
|
|
309
|
-
```ts
|
|
310
|
-
engine.snapTo('half');
|
|
311
|
-
engine.open(); // → first allowed snap with resolved size > 0
|
|
312
|
-
engine.close(reason?); // → first snap that resolves to size 0, else first allowed
|
|
313
|
-
engine.expand(); // → largest allowed snap
|
|
314
|
-
engine.collapse(); // → smallest allowed snap with size > 0
|
|
315
|
-
engine.setAllowed(ids, snapId); // dynamic allowlist
|
|
316
|
-
engine.setSnapPoints(points); // re-measure
|
|
317
|
-
engine.recompute(); // re-measure a 'fit' / 'content' snap on demand
|
|
318
|
-
engine.getResolvedSnaps(); // → [{ id, size }] resolved to pixels right now
|
|
319
|
-
engine.setPersistent(bool); // toggle persistence (block dismissal) at runtime
|
|
320
|
-
engine.setDisableClose(bool); // toggle close-blocking at runtime
|
|
321
|
-
engine.setDisableDrag(bool); // toggle the drag gesture at runtime
|
|
322
|
-
engine.setRadius('28px'); // corner radius
|
|
323
|
-
engine.setMaxHeight('92dvh'); // height cap (re-resolved on viewport change)
|
|
324
|
-
engine.canDismiss(); // false when persistent / disableClose
|
|
325
|
-
engine.isTop(); // top of the multi-sheet stack?
|
|
326
|
-
engine.depth(); // open sheets above this one
|
|
327
|
-
engine.destroy(); // remove listeners + cancel animation
|
|
328
|
-
```
|
|
329
|
-
|
|
330
|
-
`initial` starts the sheet **born open** at any snap whose size resolves `> 0`
|
|
331
|
-
(no `open` / `opened` event fires — there is no transition to announce). Calling
|
|
332
|
-
`open()` / `snapTo()` **synchronously in the mount tick is safe** — no
|
|
333
|
-
`requestAnimationFrame` / `nextTick` delay is needed, including `fit` / `content`
|
|
334
|
-
and teleported sheets. A sheet mounted hidden (`display:none` ancestor) heals
|
|
335
|
-
and emits its open sequence once on reveal. See
|
|
336
|
-
[Architecture → Opening at construction](docs/ARCHITECTURE.md).
|
|
233
|
+
<details>
|
|
234
|
+
<summary><b>Custom element</b> (Angular, Lit, Astro, plain HTML)</summary>
|
|
337
235
|
|
|
338
|
-
|
|
236
|
+
```html
|
|
237
|
+
<script type="module">
|
|
238
|
+
import '@surdeddd/bottom-sheet/element';
|
|
239
|
+
</script>
|
|
339
240
|
|
|
340
|
-
|
|
341
|
-
scrim properties can be mutated at runtime — no remount needed.
|
|
241
|
+
<button onclick="document.querySelector('bottom-sheet').open()">Filters</button>
|
|
342
242
|
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
| `setScrimColor(color \| null)` / `setScrimBlur(blur \| null)` | Live color & backdrop-filter blur. `null` clears. |
|
|
349
|
-
| `setScrimInteractive(boolean)` | Toggle `pointer-events` on the scrim layer. |
|
|
350
|
-
| `setBackdropRange([s, e])` / `setScreenRange([s, e])` | Opacity-range mapping (progress 0..1 → opacity 0..1). |
|
|
351
|
-
| `setScrim({ ... })` | Batch — applies preset + individual fields in one pass with a single `applySize`. |
|
|
352
|
-
| `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. |
|
|
353
|
-
| `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
|
+
```
|
|
354
248
|
|
|
355
|
-
|
|
249
|
+
The element carries its own styles in its shadow root; no stylesheet is needed.
|
|
250
|
+
Without a bundler, load it from a CDN:
|
|
356
251
|
|
|
357
|
-
|
|
252
|
+
```html
|
|
253
|
+
<script src="https://unpkg.com/@surdeddd/bottom-sheet/dist/element.iife.global.js"></script>
|
|
254
|
+
```
|
|
358
255
|
|
|
359
|
-
|
|
360
|
-
panel that mirrors the scrim's runtime-mutability story for content.
|
|
256
|
+
[Custom element guide →](docs/web-component.md)
|
|
361
257
|
|
|
362
|
-
|
|
363
|
-
| --- | --- |
|
|
364
|
-
| `setOverlayChildren(node \| fragment \| factory)` | Replace overlay contents at runtime. Pass a `Node`, a `DocumentFragment`, or a `() => Node` factory. |
|
|
365
|
-
| `clearOverlayChildren()` | Remove any injected content. |
|
|
366
|
-
| `setOverlay({ children, ... })` | Batch update — `children` is one of the accepted fields alongside positioning/preset overrides. |
|
|
258
|
+
</details>
|
|
367
259
|
|
|
368
|
-
|
|
369
|
-
|
|
260
|
+
<details>
|
|
261
|
+
<summary><b>No framework</b></summary>
|
|
370
262
|
|
|
371
|
-
|
|
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
|
+
```
|
|
372
272
|
|
|
373
273
|
```ts
|
|
374
|
-
import {
|
|
274
|
+
import { BottomSheetEngine } from '@surdeddd/bottom-sheet';
|
|
275
|
+
import '@surdeddd/bottom-sheet/styles';
|
|
375
276
|
|
|
376
|
-
const
|
|
377
|
-
|
|
378
|
-
|
|
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',
|
|
379
288
|
});
|
|
380
289
|
|
|
381
|
-
|
|
382
|
-
sheets.transition(prev, next);
|
|
383
|
-
const cfg = sheets.resolve(currentRoute); // typed!
|
|
290
|
+
document.querySelector('#filters')!.addEventListener('click', () => sheet.open());
|
|
384
291
|
```
|
|
385
292
|
|
|
386
|
-
|
|
293
|
+
[Vanilla guide →](docs/vanilla.md)
|
|
387
294
|
|
|
388
|
-
|
|
295
|
+
</details>
|
|
389
296
|
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
240 Hz internally for stability, but the engine writes to the DOM exactly
|
|
395
|
-
once per `requestAnimationFrame`.
|
|
396
|
-
3. **Settled-state-only React snapshot.** `useSyncExternalStore` refreshes
|
|
397
|
-
only on `snap` / `dragstart` / `dragend`. Drag pixels and animation frames
|
|
398
|
-
propagate via CSS variables (`--bs-progress`, `--bs-size`) and direct
|
|
399
|
-
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).
|
|
400
301
|
|
|
401
|
-
##
|
|
302
|
+
## Concepts
|
|
402
303
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
~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`:
|
|
406
306
|
|
|
407
307
|
```ts
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
features: [routeFeature(), contentSwipeFeature()],
|
|
415
|
-
});
|
|
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)"
|
|
416
314
|
```
|
|
417
315
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
|
427
428
|
|
|
428
429
|
```ts
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
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();
|
|
435
454
|
```
|
|
436
455
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
sheet bends as one surface, and handed straight back to the DOM once it
|
|
440
|
-
settles. Layout, scrolling, gestures and the accessibility tree never leave the
|
|
441
|
-
DOM. 4.2 KB gzip, and only if you import it.
|
|
442
|
-
Falls back to the DOM renderer, silently and mid-session if it has to, when
|
|
443
|
-
there is no WebGL, under `prefers-reduced-motion`, or when the browser drops
|
|
444
|
-
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.
|
|
445
458
|
|
|
446
|
-
|
|
459
|
+
When `setAllowed()` or `setSnapPoints()` takes the active point away, an open
|
|
460
|
+
sheet moves to the nearest point that keeps it open.
|
|
447
461
|
|
|
448
|
-
|
|
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()`.
|
|
449
465
|
|
|
450
|
-
|
|
451
|
-
import { attachDebugOverlay } from '@surdeddd/bottom-sheet/debug';
|
|
466
|
+
### Events
|
|
452
467
|
|
|
453
|
-
|
|
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 }) => {});
|
|
454
482
|
```
|
|
455
483
|
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
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.
|
|
459
495
|
|
|
460
|
-
###
|
|
496
|
+
### The keyboard
|
|
461
497
|
|
|
462
|
-
The
|
|
463
|
-
`
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
configuration — `import { BottomSheet } from '@surdeddd/bottom-sheet/qwik'`
|
|
467
|
-
keeps working for CSR, and Qwik City SSR now works out of the box.
|
|
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.
|
|
468
502
|
|
|
469
|
-
|
|
503
|
+
### More of the API
|
|
504
|
+
|
|
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) |
|
|
470
515
|
|
|
471
|
-
|
|
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.
|
|
472
520
|
|
|
473
521
|
```css
|
|
474
522
|
.bs-root {
|
|
475
523
|
--bs-surface: #18181b;
|
|
476
|
-
--bs-handle-color: #71717a;
|
|
477
524
|
--bs-radius: 28px;
|
|
478
|
-
--bs-
|
|
525
|
+
--bs-handle-color: #71717a;
|
|
526
|
+
--bs-backdrop-color: rgb(0 0 0 / 0.6);
|
|
479
527
|
}
|
|
480
528
|
```
|
|
481
529
|
|
|
482
|
-
The engine writes `--bs-progress` (0
|
|
483
|
-
|
|
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:
|
|
484
533
|
|
|
485
534
|
```css
|
|
486
|
-
.
|
|
487
|
-
|
|
488
|
-
}
|
|
489
|
-
.map-canvas {
|
|
490
|
-
filter: blur(calc(var(--bs-progress) * 8px));
|
|
491
|
-
}
|
|
492
|
-
.search-input {
|
|
493
|
-
opacity: calc(1 - var(--bs-progress));
|
|
494
|
-
}
|
|
535
|
+
.bs-root .map-preview { filter: blur(calc(var(--bs-progress) * 8px)); }
|
|
536
|
+
.bs-root .fab { bottom: calc(var(--bs-size) + 16px); }
|
|
495
537
|
```
|
|
496
538
|
|
|
497
|
-
|
|
498
|
-
> `.bs-sheet`, `.bs-backdrop` and anchor wrappers on every stack change (base
|
|
499
|
-
> `100`, step `10` per depth), so a value you set is overwritten and races the
|
|
500
|
-
> stack. To position the sheet layer against app UI, move the container
|
|
501
|
-
> (teleport target / `.bs-root` parent) or change the open order — don't style
|
|
502
|
-
> `z-index` on bs-managed elements. See
|
|
503
|
-
> [Architecture → z-index & the sheet stack](docs/ARCHITECTURE.md).
|
|
504
|
-
|
|
505
|
-
**Every sheet owns its backdrop.** Open a list, then a detail sheet over it,
|
|
506
|
-
and both backdrops stay live: the dimming deepens with each level, and the
|
|
507
|
-
sheet underneath stays masked for the whole time the one above it closes, so
|
|
508
|
-
taps never reach the page mid-animation. Each backdrop follows its own sheet's
|
|
509
|
-
progress, so a buried sheet that closes clears its own scrim. The stack keeps
|
|
510
|
-
the order sheets started opening in — a sheet opened while another is still
|
|
511
|
-
animating stays on top. If you want a single dim level, pass `backdrop={false}`
|
|
512
|
-
(`:backdrop="false"` in Vue) to the sheets you open on top.
|
|
513
|
-
|
|
514
|
-
### Content reachable at every snap
|
|
515
|
-
|
|
516
|
-
The sheet is always as tall as its largest snap point and is moved with a
|
|
517
|
-
transform; that is what keeps a drag on the compositor. The cost is that below
|
|
518
|
-
the largest snap the bottom of the scroll container sits under the screen edge,
|
|
519
|
-
by exactly `largest snap − current size`. At a half snap you can scroll, but the
|
|
520
|
-
last rows never come into view until the sheet is pulled up.
|
|
521
|
-
|
|
522
|
-
`fitContentToSnap` closes that gap:
|
|
539
|
+
Three ready-made looks ship as extra stylesheets:
|
|
523
540
|
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
<LongList />
|
|
527
|
-
</BottomSheet>
|
|
528
|
-
```
|
|
529
|
-
|
|
530
|
-
When the sheet settles, the engine writes the hidden amount to
|
|
531
|
-
`--bs-content-inset` and the stylesheet shrinks `.bs-content` by exactly that
|
|
532
|
-
much, so the scroll container ends at the edge of the screen: the last row, a
|
|
533
|
-
focused field and the scrollbar all stay in the visible part, and the thumb is
|
|
534
|
-
sized for what you can actually see. The moment the sheet starts to move, it
|
|
535
|
-
switches to the full-height container with a spacer of the same size at the end
|
|
536
|
-
of the list, so the rows below the edge are already laid out when a drag or an
|
|
537
|
-
animation reveals them. The switch changes the box and the scroll range by the
|
|
538
|
-
same amount, so nothing on screen moves. That is one layout when motion starts
|
|
539
|
-
and one when it settles, none per frame, so the motion stays transform-only.
|
|
540
|
-
While the sheet moves, a list that was scrolled to its very end is held in place
|
|
541
|
-
on screen instead of riding up with the sheet and dropping back at the end.
|
|
542
|
-
|
|
543
|
-
The footer has the same problem and gets the same cure. It is the last thing in
|
|
544
|
-
the sheet, so below the largest snap it sits under the screen edge along with
|
|
545
|
-
the end of the list. With `fitContentToSnap` on, the shrunken container puts the
|
|
546
|
-
footer at the bottom of the visible part of the sheet at rest, and while the
|
|
547
|
-
sheet moves the stylesheet lifts `.bs-footer` by `--bs-size − --bs-max-size`, so
|
|
548
|
-
it rides that edge on every frame of a drag or an animation, still without a
|
|
549
|
-
layout. The list ends above it, not under it. On a snap too short to hold both,
|
|
550
|
-
the footer slides under the screen edge instead of covering the handle. The
|
|
551
|
-
WebGL renderer gets the same layout; while the sheet moves its footer paints
|
|
552
|
-
the surface colour, and a drag lifts the list into a texture that follows the
|
|
553
|
-
list's scroll position, so a list held at its end stays put there as well.
|
|
554
|
-
|
|
555
|
-
#### Scrolling the list at every snap on touch
|
|
556
|
-
|
|
557
|
-
Making the end of the list *reachable* and letting a finger *scroll* to it are
|
|
558
|
-
two separate things. By default a swipe that starts on the content drags the
|
|
559
|
-
sheet until it reaches its largest snap, and only then scrolls the list — the
|
|
560
|
-
native sheet behaviour. On a mouse or trackpad the wheel scrolls at any snap
|
|
561
|
-
regardless; on touch it does not. To scroll at a half snap with a finger, hand
|
|
562
|
-
content swipes to the list and keep the handle (and header) for moving the
|
|
563
|
-
sheet:
|
|
564
|
-
|
|
565
|
-
```tsx
|
|
566
|
-
<BottomSheet snapPoints={points} fitContentToSnap dragFromContent={false}>
|
|
567
|
-
<LongList />
|
|
568
|
-
</BottomSheet>
|
|
541
|
+
```ts
|
|
542
|
+
import '@surdeddd/bottom-sheet/themes/ios'; // also /material, /vercel
|
|
569
543
|
```
|
|
570
544
|
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
```ts
|
|
575
|
-
const points = [
|
|
576
|
-
{ id: "peek", size: 120 },
|
|
577
|
-
{ id: "half", size: "50%", dragFromContent: false },
|
|
578
|
-
{ id: "full", size: "90%", dragFromContent: false },
|
|
579
|
-
];
|
|
545
|
+
```html
|
|
546
|
+
<div class="bs-theme-ios"><!-- the sheet --></div>
|
|
580
547
|
```
|
|
581
548
|
|
|
582
|
-
|
|
583
|
-
child of the sheet's flex column, the way `.bs-content` is. For any other
|
|
584
|
-
container, `data-bs-fit-content` on the sheet reads `rest` or `moving` and
|
|
585
|
-
`--bs-content-inset` holds the hidden amount: shrink the container by it at
|
|
586
|
-
rest, or pad the end of its content by it while moving. On the slim core add
|
|
587
|
-
`contentFitFeature()` from `@surdeddd/bottom-sheet/features`.
|
|
549
|
+
[All tokens, themes, stacking and the custom element's parts →](docs/theming.md)
|
|
588
550
|
|
|
589
|
-
|
|
551
|
+
## Accessibility
|
|
590
552
|
|
|
591
|
-
|
|
592
|
-
| ------------------------ | ----------- | ---------------------------------------------------- |
|
|
593
|
-
| `--bs-scrim-color` | transparent | Fill of the scrim layer (`.bs-screen`) |
|
|
594
|
-
| `--bs-scrim-blur` | `0px` | `backdrop-filter: blur()` applied to the scrim layer |
|
|
595
|
-
| `--bs-header-min-height` | `0px` | Minimum height of the `.bs-header` region |
|
|
553
|
+
What is built in, and what you switch on:
|
|
596
554
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
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.
|
|
602
568
|
|
|
603
|
-
|
|
569
|
+
A modal sheet should have `focusTrap` and `inertSiblings` both on.
|
|
604
570
|
|
|
605
|
-
|
|
606
|
-
Defaults match the built-in look. Set the variable on the host / `.bs-root` /
|
|
607
|
-
`:root`; custom properties inherit through the Web Component's shadow boundary,
|
|
608
|
-
so the same override works for every adapter:
|
|
571
|
+
## Forms and the on-screen keyboard
|
|
609
572
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
| `--bs-header-padding` | `0 16px 8px` | header slot |
|
|
615
|
-
| `--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.
|
|
616
577
|
|
|
617
|
-
|
|
618
|
-
keeps action buttons clear of the home indicator.)
|
|
578
|
+
To stop a sheet with unsaved input from closing, veto the move:
|
|
619
579
|
|
|
620
|
-
```
|
|
621
|
-
.
|
|
622
|
-
.
|
|
623
|
-
|
|
580
|
+
```ts
|
|
581
|
+
sheet.on('before-close', ({ cancel }) => {
|
|
582
|
+
if (form.isDirty) cancel();
|
|
583
|
+
});
|
|
624
584
|
```
|
|
625
585
|
|
|
626
|
-
|
|
627
|
-
a `'content'` / `'fit'` snap taller than the cap settles flush against the edge
|
|
628
|
-
and the body scrolls internally — the sheet never lifts off its anchor and
|
|
629
|
-
`--bs-size` always equals the rendered height.
|
|
586
|
+
For React Hook Form and Formik this is one line:
|
|
630
587
|
|
|
631
|
-
|
|
588
|
+
```ts
|
|
589
|
+
import { useFormGuard } from '@surdeddd/bottom-sheet/integrations/react-hook-form';
|
|
632
590
|
|
|
633
|
-
|
|
634
|
-
npm run demo
|
|
635
|
-
# http://localhost:5173
|
|
591
|
+
useFormGuard(sheetRef, form); // stays put while submitting, stays open while dirty
|
|
636
592
|
```
|
|
637
593
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
594
|
+
## Server rendering
|
|
595
|
+
|
|
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
|
-
##
|
|
602
|
+
## Slim core
|
|
643
603
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
resolve against the direction that applies to the sheet, so `start` is the left
|
|
647
|
-
edge in English and the right edge in Arabic:
|
|
604
|
+
The default engine includes every optional behaviour. If size matters more,
|
|
605
|
+
start from the slim core and add the features you use:
|
|
648
606
|
|
|
649
607
|
```ts
|
|
650
|
-
|
|
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
|
+
});
|
|
651
616
|
```
|
|
652
617
|
|
|
653
|
-
|
|
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.
|
|
654
630
|
|
|
655
631
|
## Browser support
|
|
656
632
|
|
|
657
|
-
|
|
658
|
-
ES2020 build
|
|
659
|
-
Events, CSS custom properties, `ResizeObserver
|
|
660
|
-
|
|
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)
|
|
661
638
|
|
|
662
|
-
|
|
639
|
+
## Documentation
|
|
663
640
|
|
|
664
|
-
|
|
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) |
|
|
665
649
|
|
|
666
|
-
|
|
667
|
-
npm test # 500+ unit tests via vitest + happy-dom
|
|
668
|
-
npx playwright test # e2e via Playwright on mobile-Chrome / mobile-Safari / Firefox
|
|
669
|
-
npm run typecheck # TypeScript --noEmit
|
|
670
|
-
```
|
|
650
|
+
## For AI coding assistants
|
|
671
651
|
|
|
672
|
-
|
|
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.
|
|
673
655
|
|
|
674
656
|
## Contributing
|
|
675
657
|
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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).
|
|
681
667
|
|
|
682
668
|
## License
|
|
683
669
|
|
|
684
|
-
MIT
|
|
670
|
+
MIT, see [LICENSE](LICENSE).
|