Sheet
Extends the Dialog component to display content that complements the main content of the screen.
Examples
Basic
| Prop | Default | Type | Description |
|---|---|---|---|
title | - | string | The title of the sheet. |
description | - | string | The description of the sheet. |
showClose | true | boolean | Show the close button. |
defaultOpen | false | boolean | The open state of the sheet when it is initially rendered. Use when you do not need to control its open state. |
modal | true | boolean | The modality of the sheet. When set to true, interaction with outside elements will be disabled and only sheet content will be visible to screen readers. |
open | - | boolean | The controlled open state of the sheet. Can be binded as v-model:open. |
overlay | true | boolean | Show the overlay. |
Read more in Reka Sheet Root API
Preview
Code
<script setup lang="ts">
const username = ref('')
</script>
<template>
<NSheet
title="Sheet Title"
description="This is a basic example of the sheet component."
>
<template #trigger>
<NButton btn="outline-gray">
Open Sheet
</NButton>
</template>
<template #body>
<div class="grid gap-4 p-4">
<div class="grid grid-cols-4 items-center gap-4">
<NLabel for="name" class="text-right">
Name
</NLabel>
<NInput id="name" v-model="username" :una="{ inputWrapper: 'col-span-3' }" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<NLabel for="username" class="text-right">
Username
</NLabel>
<NInput id="username" v-model="username" :una="{ inputWrapper: 'col-span-3' }" />
</div>
</div>
</template>
<template #footer>
<NSheetClose>
<NButton type="submit" label="Save changes" />
</NSheetClose>
</template>
</NSheet>
</template>
Variants
| Prop | Default | Type | Description |
|---|---|---|---|
sheet | right | string | The side from which the sheet will appear, the predefined presets are top, right, bottom, left, You can also pass a custom value to use a custom variant. |
Read more in Sheet Variants presets
Preview
Code
<script setup lang="ts">
const SHEET_SIDES = [
{
sheet: 'top',
label: 'Top',
},
{
sheet: 'right',
label: 'Right',
},
{
sheet: 'bottom',
label: 'Bottom',
},
{
sheet: 'left',
label: 'Left',
},
] as const
const username = ref('')
</script>
<template>
<div class="grid grid-cols-2 gap-2">
<!-- Side variants -->
<NSheet
v-for="side in SHEET_SIDES"
:key="side.sheet"
:sheet="side.sheet"
title="Edit profile"
description="Make changes to your profile here. Click save when you're done."
>
<template #trigger>
<NButton btn="outline-gray">
Open {{ side.label }}
</NButton>
</template>
<template #body>
<div class="grid gap-4 p-4">
<div class="grid grid-cols-4 items-center gap-4">
<NLabel for="name" class="text-right">
Name
</NLabel>
<NInput id="name" v-model="username" :una="{ inputWrapper: 'col-span-3' }" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<NLabel for="username" class="text-right">
Username
</NLabel>
<NInput id="username" v-model="username" :una="{ inputWrapper: 'col-span-3' }" />
</div>
</div>
</template>
<template #footer>
<NSheetClose>
<NButton type="submit" label="Save changes" />
</NSheetClose>
</template>
</NSheet>
</div>
</template>
Prevent Closing
| Prop | Default | Type | Description |
|---|---|---|---|
dismissible | true | boolean | If false, the sheet will not close on overlay click or escape key press. |
Preview
Code
<template>
<div>
<NSheet
sheet="bottom"
:dismissible="false"
title="Prevent close"
description="This sheet cannot be closed by clicking outside of it"
>
<template #trigger>
<NButton btn="outline-gray">
Prevent close
</NButton>
</template>
<template #body>
<div class="grid gap-4 p-4">
<div class="grid grid-cols-4 items-center gap-4">
<NSkeleton class="h-4 w-[60px] justify-self-end" />
<NSkeleton class="col-span-3 h-10" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<NSkeleton class="h-4 w-[80px] justify-self-end" />
<NSkeleton class="col-span-3 h-10" />
</div>
</div>
</template>
<template #footer>
<NSheetClose>
<NButton type="submit" label="Save changes" />
</NSheetClose>
</template>
</NSheet>
</div>
</template>
Customization
You can customize the sheet using the following sub components props and una prop.
| Name | Props | Type | Description |
|---|---|---|---|
_sheetTrigger | - | object | The trigger button props. |
_sheetContent | - | object | The content props. |
_sheetHeader | - | object | The header props. |
_sheetFooter | - | object | The footer props. |
_sheetTitle | - | object | The title props. |
_sheetDescription | - | object | The description props. |
_sheetClose | - | object | The close button props. |
_sheetOverlay | - | object | The overlay props. |
_sheetPortal | - | object | The portal props. |
una | - | object | The una preset props. |
Read more in Sheet props
Size Customization
Preview
Code
<template>
<div>
<NSheet
title="Users"
description="Manage your users"
:una="{
sheetContent: 'max-w-7xl overflow-y-auto',
}"
>
<template #trigger>
<NButton btn="outline-gray">
Adjust Sheet Size
</NButton>
</template>
<template #body>
<div class="overflow-auto p-4">
<ExampleVueTableSlots />
</div>
</template>
</NSheet>
</div>
</template>
Icon Customization
Preview
Code
<template>
<div>
<NSheet
sheet="right"
title="Custom Icon"
description="This sheet uses a custom close icon"
:_sheet-close="{
label: 'i-lucide-arrow-right-from-line',
btn: 'solid-black',
}"
>
<template #trigger>
<NButton btn="outline-gray">
Custom Icon
</NButton>
</template>
<template #body>
<div class="grid gap-4 p-4">
<div class="grid grid-cols-4 items-center gap-4">
<NSkeleton class="h-4 w-[60px] justify-self-end" />
<NSkeleton class="col-span-3 h-10" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<NSkeleton class="h-4 w-[80px] justify-self-end" />
<NSkeleton class="col-span-3 h-10" />
</div>
</div>
</template>
<template #footer>
<NSheetClose>
<NButton type="submit" label="Save changes" />
</NSheetClose>
</template>
</NSheet>
</div>
</template>
Overlay Customization
Preview
Code
<template>
<div>
<NSheet
title="Blur Overlay"
description="This sheet uses a blur overlay"
:una="{
sheetOverlay: 'backdrop-blur-sm',
}"
>
<template #trigger>
<NButton btn="outline-gray">
Blur Overlay
</NButton>
</template>
<template #body>
<div class="grid gap-4 p-4">
<div class="grid grid-cols-4 items-center gap-4">
<NSkeleton class="h-4 w-[60px] justify-self-end" />
<NSkeleton class="col-span-3 h-10" />
</div>
<div class="grid grid-cols-4 items-center gap-4">
<NSkeleton class="h-4 w-[80px] justify-self-end" />
<NSkeleton class="col-span-3 h-10" />
</div>
</div>
</template>
<template #footer>
<NSheetClose>
<NButton type="submit" label="Save changes" />
</NSheetClose>
</template>
</NSheet>
</div>
</template>
Slots
| Name | Props | Description |
|---|---|---|
default | - | Allows advanced customization using sub-components, replacing the default sheet structure. |
content | - | The entire content slot, includes the header, title, description, footer and body. |
trigger | open | The trigger button used to open the sheet. |
header | - | Contains the title and description slots. |
title | - | The title displayed in the sheet. |
description | - | The description displayed below the title. |
body | - | The body slot. |
footer | - | The footer. |
Custom Rendering
Use the default slot for full control over the sheet's structure. This allows you to compose the sheet using its individual sub-components (like NSheetContent, NSheetTrigger, etc., listed in the Components section), similar to libraries like shadcn/ui.
Preview
Code
<script setup lang="ts">
const SHEET_SIDES = [
{ sheet: 'top' },
{ sheet: 'right' },
{ sheet: 'bottom' },
{ sheet: 'left' },
] as const
</script>
<template>
<div class="flex flex-col gap-6 md:flex-row">
<NSheet>
<NSheetTrigger as-child>
<NButton btn="outline-gray">
Open
</NButton>
</NSheetTrigger>
<NSheetContent>
<NSheetHeader>
<NSheetTitle>Edit profile</NSheetTitle>
<NSheetDescription>
Make changes to your profile here. Click save when you're
done.
</NSheetDescription>
</NSheetHeader>
<div class="grid auto-rows-min flex-1 gap-6 px-4">
<div class="grid gap-3">
<NLabel for="sheet-demo-name">
Name
</NLabel>
<NInput id="sheet-demo-name" default-value="Pedro Duarte" />
</div>
<div class="grid gap-3">
<NLabel for="sheet-demo-username">
Username
</NLabel>
<NInput id="sheet-demo-username" default-value="@peduarte" />
</div>
</div>
<NSheetFooter>
<NButton type="submit">
Save changes
</NButton>
<NSheetClose as-child>
<NButton btn="outline-gray">
Close
</NButton>
</NSheetClose>
</NSheetFooter>
</NSheetContent>
</NSheet>
<div class="flex gap-2">
<NSheet v-for="{ sheet } in SHEET_SIDES" :key="sheet">
<NSheetTrigger as-child>
<NButton btn="outline-gray" class="capitalize">
{{ sheet }}
</NButton>
</NSheetTrigger>
<NSheetContent :sheet="sheet">
<NSheetHeader>
<NSheetTitle>Edit profile</NSheetTitle>
<NSheetDescription>
Make changes to your profile here. Click save when you're
done.
</NSheetDescription>
</NSheetHeader>
<div class="overflow-y-auto px-4 text-sm">
<h4 class="mb-4 text-lg font-medium leading-none">
Lorem Ipsum
</h4>
<p v-for="index in 10" :key="index" class="mb-4 leading-normal">
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed
do eiusmod tempor incididunt ut labore et dolore magna
aliqua. Ut enim ad minim veniam, quis nostrud exercitation
ullamco laboris nisi ut aliquip ex ea commodo consequat.
Duis aute irure dolor in reprehenderit in voluptate velit
esse cillum dolore eu fugiat nulla pariatur. Excepteur sint
occaecat cupidatat non proident, sunt in culpa qui officia
deserunt mollit anim id est laborum.
</p>
</div>
<NSheetFooter>
<NButton type="submit">
Save changes
</NButton>
<NSheetClose as-child>
<NButton btn="outline-gray">
Cancel
</NButton>
</NSheetClose>
</NSheetFooter>
</NSheetContent>
</NSheet>
</div>
</div>
</template>
Presets
shortcuts/sheet.ts
type SheetPrefix = 'sheet'
export const staticSheet: Record<`${SheetPrefix}-${string}` | SheetPrefix, string> = {
// base
'sheet': '',
// sub components
'sheet-content': 'bg-background fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out',
'sheet-portal': '',
'sheet-overlay': 'fixed inset-0 z-50 bg-black/80',
'sheet-close': 'absolute right-4 top-4',
'sheet-description': 'text-sm text-muted-foreground',
'sheet-footer': 'mt-auto flex flex-col gap-2 p-4',
'sheet-header': 'flex flex-col gap-1.5 p-4',
'sheet-title': 'text-foreground font-semibold',
// static variants
'sheet-right': 'data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
'sheet-left': 'data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
'sheet-top': 'data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
'sheet-bottom': 'data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
}
export const dynamicSheet: [RegExp, (params: RegExpExecArray) => string][] = [
// dynamic preset
]
export const sheet = [
...dynamicSheet,
staticSheet,
]
Props
types/sheet.ts
import type { DialogCloseProps, DialogContentProps, DialogDescriptionProps, DialogOverlayProps, DialogPortalProps, DialogRootProps, DialogTitleProps, DialogTriggerProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import type { NButtonProps } from './button'
export interface NSheetProps extends DialogRootProps, Pick<NSheetContentProps, 'sheet' | 'dismissible' | 'showClose' | 'overlay'> {
/**
* The title of the sheet.
*/
title?: string
/**
* The description of the sheet.
*/
description?: string
// sub components
_sheetTrigger?: NSheetTriggerProps
_sheetContent?: NSheetContentProps
_sheetHeader?: NSheetHeaderProps
_sheetFooter?: NSheetFooterProps
_sheetTitle?: NSheetTitleProps
_sheetDescription?: NSheetDescriptionProps
_sheetClose?: NSheetCloseProps
_sheetOverlay?: NSheetOverlayProps
_sheetPortal?: NSheetPortalProps
/**
* `UnaUI` preset configuration
*
* @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/sheet.ts
*/
una?: NSheetUnaProps
}
export interface NSheetContentProps extends DialogContentProps {
/**
* Additional attributes that can be passed to the sheet content element.
*/
[key: string]: any
/**
* The class of the sheet.
*/
class?: HTMLAttributes['class']
/**
* The side of the sheet.
*
* By default, preset provided `top`, `right`, `bottom`, `left` variants are available.
* You can also pass your own via unocss.config.ts
*
* @default 'right'
*/
sheet?: HTMLAttributes['class']
/**
* Prevent close.
*/
dismissible?: boolean
/**
* Show close button.
*
* @default true
*/
showClose?: boolean
/**
* Show overlay.
*
* @default true
*/
overlay?: boolean
/**
* The close button props.
*/
_sheetClose?: NSheetCloseProps
/**
* The overlay props.
*/
_sheetOverlay?: NSheetOverlayProps
/**
* The portal props.
*/
_sheetPortal?: NSheetPortalProps
/**
* `UnaUI` preset configuration
*
* @see https://github.com/una-ui/una-ui/blob/main/packages/preset/src/_shortcuts/sheet.ts
*/
una?: Pick<NSheetUnaProps, 'sheetContent' | 'sheetPortal' | 'sheetOverlay' | 'sheetClose'>
}
export interface NSheetTriggerProps extends DialogTriggerProps {
}
export interface NSheetHeaderProps {
/**
* Additional attributes that can be passed to the sheet header element.
*/
[key: string]: any
/**
* The class of the sheet header.
*/
class?: HTMLAttributes['class']
una?: Pick<NSheetUnaProps, 'sheetHeader'>
}
export interface NSheetTitleProps extends DialogTitleProps {
/**
* Additional attributes that can be passed to the sheet title element.
*/
[key: string]: any
/**
* The class of the sheet title.
*/
class?: HTMLAttributes['class']
una?: Pick<NSheetUnaProps, 'sheetTitle'>
}
export interface NSheetDescriptionProps extends DialogDescriptionProps {
/**
* Additional attributes that can be passed to the sheet description element.
*/
[key: string]: any
/**
* The class of the sheet description.
*/
class?: HTMLAttributes['class']
una?: Pick<NSheetUnaProps, 'sheetDescription'>
}
export interface NSheetFooterProps {
/**
* Additional attributes that can be passed to the sheet footer element.
*/
[key: string]: any
/**
* The class of the sheet footer.
*/
class?: HTMLAttributes['class']
una?: Pick<NSheetUnaProps, 'sheetFooter'>
}
export interface NSheetCloseProps extends DialogCloseProps, NButtonProps {
/**
* Additional attributes that can be passed to the sheet close element.
*/
[key: string]: any
/**
* The class of the sheet close.
*/
}
export interface NSheetOverlayProps extends DialogOverlayProps {
/**
* Additional attributes that can be passed to the sheet overlay element.
*/
[key: string]: any
/**
* The class of the sheet overlay.
*/
class?: HTMLAttributes['class']
una?: Pick<NSheetUnaProps, 'sheetOverlay'>
}
export interface NSheetPortalProps extends DialogPortalProps {
/**
* Additional attributes that can be passed to the sheet portal element.
*/
[key: string]: any
/**
* The class of the sheet portal.
*/
class?: HTMLAttributes['class']
una?: Pick<NSheetUnaProps, 'sheetPortal'>
}
export interface NSheetUnaProps {
sheet?: HTMLAttributes['class']
sheetContent?: HTMLAttributes['class']
sheetClose?: HTMLAttributes['class']
sheetHeader?: HTMLAttributes['class']
sheetTitle?: HTMLAttributes['class']
sheetDescription?: HTMLAttributes['class']
sheetFooter?: HTMLAttributes['class']
sheetOverlay?: HTMLAttributes['class']
sheetPortal?: HTMLAttributes['class']
}
Components
Sheet.vue
SheetContent.vue
SheetTitle.vue
SheetDescription.vue
SheetHeader.vue
SheetFooter.vue
SheetClose.vue
<script setup lang="ts">
import type { DialogRootEmits } from 'reka-ui'
import type { NSheetProps } from '../../types'
import { reactivePick } from '@vueuse/core'
import { DialogRoot, useForwardPropsEmits, VisuallyHidden } from 'reka-ui'
import { computed } from 'vue'
import { randomId } from '../../utils'
import SheetContent from './SheetContent.vue'
import SheetDescription from './SheetDescription.vue'
import SheetFooter from './SheetFooter.vue'
import SheetHeader from './SheetHeader.vue'
import SheetTitle from './SheetTitle.vue'
import SheetTrigger from './SheetTrigger.vue'
const props = withDefaults(defineProps<NSheetProps>(), {
showClose: true,
overlay: true,
dismissible: true,
})
const emits = defineEmits<DialogRootEmits>()
const DEFAULT_TITLE = randomId('sheet-title')
const DEFAULT_DESCRIPTION = randomId('sheet-description')
const title = computed(() => props.title || DEFAULT_TITLE)
const description = computed(() => props.description || DEFAULT_DESCRIPTION)
const rootProps = reactivePick(props, ['open', 'defaultOpen', 'modal'])
const forwarded = useForwardPropsEmits(rootProps, emits)
</script>
<template>
<DialogRoot
v-slot="{ open }"
data-slot="sheet"
v-bind="forwarded"
>
<slot>
<SheetTrigger
v-if="$slots.trigger"
v-bind="_sheetTrigger"
>
<slot name="trigger" :open />
</SheetTrigger>
<SheetContent
:_sheet-close
:_sheet-overlay
:_sheet-portal
:sheet
:dismissible
:show-close
:overlay
v-bind="_sheetContent"
:una
>
<VisuallyHidden v-if="(title === DEFAULT_TITLE || !!$slots.title) || (description === DEFAULT_DESCRIPTION || !!$slots.description)">
<SheetTitle v-if="title === DEFAULT_TITLE || !!$slots.title">
{{ title }}
</SheetTitle>
<SheetDescription v-if="description === DEFAULT_DESCRIPTION || !!$slots.description">
{{ description }}
</SheetDescription>
</VisuallyHidden>
<slot name="content">
<SheetHeader
v-if="!!$slots.header || (title !== DEFAULT_TITLE || !!$slots.title) || (description !== DEFAULT_DESCRIPTION || !!$slots.description)"
v-bind="_sheetHeader"
:una
>
<slot name="header">
<SheetTitle
v-if="$slots.title || title !== DEFAULT_TITLE"
v-bind="_sheetTitle"
:una
>
<slot name="title">
{{ title }}
</slot>
</SheetTitle>
<SheetDescription
v-if="$slots.description || description !== DEFAULT_DESCRIPTION"
v-bind="_sheetDescription"
:una
>
<slot name="description">
{{ description }}
</slot>
</SheetDescription>
</slot>
</SheetHeader>
<slot name="body">
<slot />
</slot>
<SheetFooter
v-if="$slots.footer"
v-bind="_sheetFooter"
:una
>
<slot name="footer" />
</SheetFooter>
</slot>
</SheetContent>
</slot>
</DialogRoot>
</template>
<script setup lang="ts">
import type { DialogContentEmits } from 'reka-ui'
import type { NSheetContentProps } from '../../types'
import { reactiveOmit } from '@vueuse/core'
import { DialogContent, DialogPortal, useForwardPropsEmits } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '../../utils'
import SheetClose from './SheetClose.vue'
import SheetOverlay from './SheetOverlay.vue'
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<NSheetContentProps>(), {
sheet: 'right',
overlay: true,
showClose: true,
dismissible: true,
})
const emits = defineEmits<DialogContentEmits>()
const contentProps = reactiveOmit(props, ['sheet', 'class', '_sheetClose', '_sheetPortal', '_sheetOverlay'])
const forwarded = useForwardPropsEmits(contentProps, emits)
const contentEvents = computed(() => {
if (!props.dismissible) {
return {
pointerDownOutside: (e: Event) => e.preventDefault(),
interactOutside: (e: Event) => e.preventDefault(),
escapeKeyDown: (e: Event) => e.preventDefault(),
closeAutoFocus: (e: Event) => e.preventDefault(),
}
}
return {
closeAutoFocus: (e: Event) => e.preventDefault(),
}
})
</script>
<template>
<DialogPortal
v-bind="props._sheetPortal"
:class="cn('sheet-portal', props.una?.sheetPortal, props._sheetPortal?.class)"
>
<SheetOverlay
v-if="props.overlay"
v-bind="_sheetOverlay"
/>
<DialogContent
data-slot="sheet-content"
v-bind="{ ...forwarded, ...$attrs }"
:sheet
:class="cn(
'data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
'sheet-content',
props.una?.sheetContent,
props.class,
)"
v-on="contentEvents"
>
<slot />
<SheetClose
v-if="props.showClose"
:class="cn('sheet-close', props.una?.sheetClose)"
v-bind="props._sheetClose"
/>
</DialogContent>
</DialogPortal>
</template>
<script setup lang="ts">
import type { NSheetTitleProps } from '../../types'
import { reactiveOmit } from '@vueuse/core'
import { DialogTitle } from 'reka-ui'
import { cn } from '../../utils'
const props = defineProps<NSheetTitleProps>()
const delegatedProps = reactiveOmit(props, ['class'])
</script>
<template>
<DialogTitle
data-slot="sheet-title"
:class="cn('sheet-title', props.una?.sheetTitle, props.class)"
v-bind="delegatedProps"
>
<slot />
</DialogTitle>
</template>
<script setup lang="ts">
import type { NSheetDescriptionProps } from '../../types'
import { reactiveOmit } from '@vueuse/core'
import { DialogDescription } from 'reka-ui'
import { cn } from '../../utils'
const props = defineProps<NSheetDescriptionProps>()
const delegatedProps = reactiveOmit(props, ['class'])
</script>
<template>
<DialogDescription
data-slot="sheet-description"
:class="cn('sheet-description', props.una?.sheetDescription, props.class)"
v-bind="delegatedProps"
>
<slot />
</DialogDescription>
</template>
<script setup lang="ts">
import type { NSheetHeaderProps } from '../../types'
import { cn } from '../../utils'
const props = defineProps<NSheetHeaderProps>()
</script>
<template>
<div
data-slot="sheet-header"
:class="
cn('sheet-header', props.una?.sheetHeader, props.class)
"
>
<slot />
</div>
</template>
<script setup lang="ts">
import type { NSheetFooterProps } from '../../types'
import { cn } from '../../utils'
const props = defineProps<NSheetFooterProps>()
</script>
<template>
<div
data-slot="sheet-footer"
:class="
cn(
'sheet-footer',
props.una?.sheetFooter,
props.class,
)
"
>
<slot />
</div>
</template>
<script setup lang="ts">
import type { NSheetCloseProps } from '../../types'
import { DialogClose } from 'reka-ui'
import Button from '../elements/Button.vue'
const props = withDefaults(defineProps<NSheetCloseProps>(), {
btn: 'ghost-gray',
label: 'i-close',
square: '2em',
icon: true,
ariaLabel: 'Close',
})
</script>
<template>
<DialogClose
data-slot="sheet-close"
as-child
>
<slot>
<Button
v-bind="props"
/>
</slot>
</DialogClose>
</template>