vue-bottom-sheets
| Install | |
|---|---|
npm install vue-bottom-sheets |
|
| Latest Version: | 0.4.0 |
| Vue: | 3 |
| License: | GPL-3.0-or-later |
| Last Updated: | Jun 10, 2026 |
| Links: | npm |
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-sheets
Usage
<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> globally
Snap 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 & autofix
License
GPL-3.0-or-later © Pavel Sergienko
Related Packages
A headless, unstyled and behaviorally opinionated drawer primitive for Vue 3 wit...
Production-ready bottom sheet for Vue 3 with native iOS/Android-like drag physic...
Version History
| Version | Released | Vue | License |
|---|---|---|---|
| 0.4.0 | ^3.3.0 | GPL-3.0-or-later | |
| 0.3.0 | ^3.3.0 | GPL-3.0-or-later | |
| 0.2.0 | ^3.3.0 | GPL-3.0-or-later | |
| 0.1.1 | ^3.3.0 | GPL-3.0-or-later | |
| 0.1.0 | ^3.3.0 | GPL-3.0-or-later |