vue-bottom-sheets
v0.4.0
Published
Accessible, draggable bottom sheet component for Vue 3 with snap points — inspired by react-spring-bottom-sheet.
Maintainers
Readme
vue-bottom-sheets
Accessible, draggable bottom sheet component for Vue 3 with snap points — inspired by react-spring-bottom-sheet.
Installation
npm install vue-bottom-sheets
# or: pnpm add vue-bottom-sheetsUsage
<script setup lang="ts">
import { ref } from 'vue'
import { BottomSheet } from 'vue-bottom-sheet'
const open = ref(false)
</script>
<template>
<button @click="open = true">Open</button>
<BottomSheet v-model:open="open" aria-label="Example sheet">
<template #header>
<h2>Title</h2>
</template>
<p>Sheet content goes here.</p>
<template #footer>
<button @click="open = false">Close</button>
</template>
</BottomSheet>
</template>Global registration (plugin)
import { createApp } from 'vue'
import VueBottomSheet from 'vue-bottom-sheets'
createApp(App).use(VueBottomSheet) // registers <BottomSheet> globallySnap points
Provide a fixed array of pixel heights, or a function that computes them from the measured geometry:
<BottomSheet
v-model:open="open"
:snap-points="({ maxHeight, minHeight }) => [Math.min(minHeight, 120), maxHeight * 0.5, maxHeight - 40]"
:default-snap="({ snapPoints }) => snapPoints[1]"
/>The resolver receives { height, maxHeight, headerHeight, footerHeight, minHeight }
(all in pixels).
Imperative API
<script setup lang="ts">
import { ref } from 'vue'
import { BottomSheet } from 'vue-bottom-sheet'
const sheet = ref<InstanceType<typeof BottomSheet> | null>(null)
// Jump to the tallest snap point.
function expand() {
sheet.value?.snapTo(({ snapPoints }) => snapPoints[snapPoints.length - 1])
}
</script>
<template>
<BottomSheet ref="sheet" v-model:open="open">…</BottomSheet>
</template>Props
| Prop | Type | Default | Description |
| ---------------------- | ------------------------------------- | ------------ | ---------------------------------------------------------------- |
| open | boolean | false | Controlled open state. Use with v-model:open. |
| snapPoints | number[] \| (props) => number[] | content size | Heights (px) the sheet can rest at. |
| defaultSnap | number \| (props) => number | largest | Snap point used when opening. |
| blocking | boolean | true | Render backdrop, trap focus and lock scroll. |
| closeOnBackdropClick | boolean | true | Dismiss when the backdrop is clicked. |
| closeOnEscape | boolean | true | Dismiss when Escape is pressed. |
| scrollLocking | boolean | true | Lock body scroll while open and blocking. |
| expandOnContentDrag | boolean | false | Allow dragging the sheet from its content area. |
| handle | boolean | true | Render the built-in handle pill (and header strip). Set false to use your own handle. |
| maxHeight | number | viewport | Hard cap on sheet height (px). |
| ariaLabel | string | — | Accessible label applied as aria-label. |
| skipInitialTransition| boolean | false | Skip the open animation on first mount. |
| theme | BottomSheetTheme | — | Visual overrides (colors, radius, shadow, …). See Theming. |
Events
| Event | Payload | Description |
| -------------- | ---------- | --------------------------------------------------- |
| update:open | boolean | Emitted for v-model:open. |
| dismiss | — | User requested dismissal (backdrop/escape/drag). |
| snap | number | Sheet settled on a snap point (px). |
| spring-start | — | A transition started. |
| spring-end | — | A transition finished. |
Slots
| Slot | Slot props | Description |
| --------- | ----------------- | ------------------------------------------------------ |
| default | dragHandleProps | Scrollable body content. |
| header | dragHandleProps | Sticky header above the content; the drag handle. |
| footer | — | Sticky footer below the content (rendered if present). |
Custom drag handle
By default the sheet renders its own handle strip. To make a consumer-supplied
element the grab region instead — for example a header that already lives inside
your content — set :handle="false" and spread the dragHandleProps slot prop onto
that element:
<BottomSheet v-model:open="open" :handle="false">
<template #default="{ dragHandleProps }">
<!-- Your own header IS the drag handle; the ✕ still clicks. -->
<header class="my-header" v-bind="dragHandleProps">
<h2>Title</h2>
<button @click="open = false">✕</button>
</header>
<p>Scrollable body…</p>
</template>
</BottomSheet>dragHandleProps wires up the pointer listeners and applies touch-action: none
so the browser won't claim the touch for scrolling. A drag only begins once the
pointer moves past a small threshold, so taps/clicks on buttons inside the handle
still fire. The slot prop is also available on #header.
Theming
Pass a theme object — each key is optional and falls back to the default.
Numeric keys (maxWidth, radius, transitionDuration, zIndex) also accept a
plain number (px, px, ms, unitless):
<BottomSheet
v-model:open="open"
:theme="{
radius: 24,
background: '#111',
color: '#fff',
backdrop: 'rgba(0, 0, 0, 0.6)',
handle: 'rgba(255, 255, 255, 0.3)',
}"
/>| Key | Type | CSS variable | Default |
| -------------------- | ------------------ | -------------------------- | ---------------------------------- |
| maxWidth | string \| number | --vbs-max-width | 640px |
| radius | string \| number | --vbs-radius | 16px |
| background | string | --vbs-bg | #ffffff |
| color | string | --vbs-color | #1a1a1a |
| backdrop | string | --vbs-backdrop | rgba(0, 0, 0, 0.45) |
| handle | string | --vbs-handle | rgba(0, 0, 0, 0.18) |
| shadow | string | --vbs-shadow | 0 -8px 40px rgba(0, 0, 0, 0.18) |
| transitionDuration | string \| number | --vbs-transition-duration| 320ms |
| transitionEasing | string | --vbs-transition-easing | cubic-bezier(0.22, 1, 0.36, 1) |
| zIndex | string \| number | --vbs-z-index | 1000 |
Prefer CSS? The same --vbs-* custom properties can be set on .vbs (or any
ancestor) instead of using the prop. prefers-reduced-motion is respected
automatically.
Development
pnpm install
pnpm dev # run the playground at http://localhost:5173
pnpm test # run unit tests
pnpm build # type-check + build the library to dist/
pnpm lint # lint & autofixLicense
GPL-3.0-or-later © Pavel Sergienko
