BottomSheet
A panel that slides up from the bottom edge and holds the screen until it is dismissed. The mobile counterpart to Dialog — same modal semantics, reached by thumb instead of by pointer.
Built on reka-ui's Dialog primitives, so the focus trap, scroll lock and Escape handling come for free. On top of that it adds a grab handle and swipe-down-to-dismiss.
Basic
Bind v-model:open. The sheet sizes itself to its content up to 90% of the viewport height, then scrolls inside.
<script setup lang="ts">
import { ref } from 'vue'
import { BottomSheet, Button } from 'frappe-ui'
const open = ref(false)
</script>
<template>
<Button label="Open sheet" @click="open = true" />
<BottomSheet v-model:open="open">
<div class="p-4 pb-8 text-p-base text-ink-gray-7">
Drag the handle down, tap outside, or press
<kbd>Esc</kbd> to dismiss.
</div>
</BottomSheet>
</template>Title
title renders a centered heading in the grab-handle area and labels the sheet for screen readers. Without it the sheet is labelled "Bottom sheet".
<script setup lang="ts">
import { ref } from 'vue'
import { BottomSheet, Button } from 'frappe-ui'
const open = ref(false)
const options = ['Rename', 'Duplicate', 'Move to folder', 'Delete']
</script>
<template>
<Button label="Open with a title" @click="open = true" />
<BottomSheet v-model:open="open" title="Actions">
<div class="flex flex-col p-2 pb-8">
<button
v-for="option in options"
:key="option"
class="rounded-4 px-3 py-2.5 text-start text-p-base text-ink-gray-8 hover:bg-surface-gray-2"
@click="open = false"
>
{{ option }}
</button>
</div>
</BottomSheet>
</template>Non-dismissible
dismissible defaults to true. Set it to false to turn off all three dismiss channels at once — outside click, Escape, and swipe-down — for a sheet that has to be answered.
<script setup lang="ts">
import { ref } from 'vue'
import { BottomSheet, Button } from 'frappe-ui'
const open = ref(false)
</script>
<template>
<Button label="Open a sheet you must answer" @click="open = true" />
<BottomSheet v-model:open="open" title="Discard draft?" :dismissible="false">
<div class="p-4 pb-8">
<p class="text-p-base text-ink-gray-7">
Outside click, Escape and swipe-down are all off. The sheet closes only
through these buttons.
</p>
<div class="mt-4 flex gap-2">
<Button class="flex-1" label="Keep editing" @click="open = false" />
<Button
class="flex-1"
variant="solid"
theme="red"
label="Discard"
@click="open = false"
/>
</div>
</div>
</BottomSheet>
</template>Gestures
Dragging works from anywhere on the sheet, not just the handle. A gesture that starts inside a scrolled list scrolls that list instead of moving the sheet; it becomes a drag once the list is back at its top. A drag closes the sheet if it passes a distance threshold or ends in a fast flick, and springs back otherwise.
The gesture logic lives in the useSheetDrag composable, exported from the package root if you need the same behaviour on a surface of your own.
Events
update:open fires as soon as the sheet starts closing. after-leave fires once the close animation has finished — use it to reset state that would flicker if cleared mid-animation.
Notes
- Use
Dialogon desktop andBottomSheeton mobile. They are separate components rather than one responsive component, because the two have different dismiss affordances and different content rhythms. - The sheet takes no
actionsprop. Put buttons in the default slot, where they can sit inside the scroll region or below it as the layout needs.
API Reference
Show types
export interface BottomSheetProps {
/** Controls whether the sheet is open (`v-model:open`). */
open?: boolean
/** Optional centered title rendered in the drag-handle area. */
title?: string
/** Allow outside-click, Escape, and swipe-down to close. Default `true`. */
dismissible?: boolean
}
export interface BottomSheetEmits {
/** Fired when the open state changes via `v-model:open`. */
'update:open': [value: boolean]
/** Fired after the close animation finishes. */
'after-leave': []
}
export interface BottomSheetSlots {
/** Sheet body, rendered in a scrollable region below the handle. */
default?: () => any
}Controls whether the sheet is open (`v-model:open`).
Optional centered title rendered in the drag-handle area.
Allow outside-click, Escape, and swipe-down to close. Default `true`.
| Slot | Payload |
|---|---|
default | {} |
| Event | Payload |
|---|---|
update:open | [value: boolean] Fired when the open state changes via `v-model:open`. |
after-leave | [] Fired after the close animation finishes. |
Fired when the open state changes via `v-model:open`.
Fired after the close animation finishes.