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>
<div class="grid gap-4 py-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 #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>
<div class="grid gap-4 py-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 #footer>
<NSheetClose>
<NButton type="submit" label="Save changes" />
</NSheetClose>
</template>
</NSheet>
</div>
</template>
Prevent Closing
Prop | Default | Type | Description |
---|---|---|---|
preventClose | - | boolean | If true, the sheet will not close on overlay click or escape key press. |
Preview
Code
<template>
<div>
<NSheet
sheet="bottom"
prevent-close
title="Prevent close"
description="This sheet cannot be closed by clicking outside of it"
>
<template #trigger>
<NButton btn="outline-gray">
Prevent close
</NButton>
</template>
<div class="grid gap-4 py-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 #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>
<div class="overflow-auto py-6">
<ExampleVueTableSlots />
</div>
</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>
<div class="grid gap-4 py-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 #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>
<div class="grid gap-4 py-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 #footer>
<NSheetClose>
<NButton type="submit" label="Save changes" />
</NSheetClose>
</template>
</NSheet>
</div>
</template>
Slots
Name | Props | Description |
---|---|---|
default | - | The body slot. |
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. |
footer | - | The footer. |
Presets
shortcuts/sheet.ts
type SheetPrefix = 'sheet'
export const staticSheet: Record<`${SheetPrefix}-${string}` | SheetPrefix, string> = {
// base
'sheet': '',
// sub components
'sheet-content': 'fixed z-50 gap-4 bg-base p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500',
'sheet-portal': '',
'sheet-overlay': 'fixed inset-0 z-50 data-[state=closed]:animate-out data-[state=open]:animate-in bg-black/80 data-[state=open]:fade-in-0 data-[state=closed]:fade-out-0',
'sheet-close': 'absolute right-4 top-4',
'sheet-description': 'text-sm text-muted',
'sheet-footer': 'flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2',
'sheet-header': 'flex flex-col gap-y-2 text-center sm:text-left',
'sheet-title': 'text-lg font-semibold text-base',
// static variants
'sheet-top': 'inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top',
'sheet-right': 'inset-y-0 right-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm',
'sheet-bottom': 'inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom',
'sheet-left': 'inset-y-0 left-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm',
}
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' | 'preventClose' | '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.
*/
preventClose?: 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,
})
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 }" v-bind="forwarded">
<slot name="root">
<SheetTrigger
v-if="$slots.trigger"
v-bind="_sheetTrigger"
>
<slot name="trigger" :open />
</SheetTrigger>
<SheetContent
:_sheet-close
:_sheet-overlay
:_sheet-portal
:sheet
:prevent-close
: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 />
<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, DialogOverlay, DialogPortal, useForwardPropsEmits } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '../../utils'
import SheetClose from './SheetClose.vue'
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<NSheetContentProps>(), {
sheet: 'right',
overlay: true,
showClose: true,
})
const emits = defineEmits<DialogContentEmits>()
const contentProps = reactiveOmit(props, ['sheet', 'class', '_sheetClose', '_sheetPortal', '_sheetOverlay'])
const forwarded = useForwardPropsEmits(contentProps, emits)
const contentEvents = computed(() => {
if (props.preventClose) {
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)"
>
<DialogOverlay
v-if="props.overlay"
v-bind="_sheetOverlay"
:class="cn('sheet-overlay', props.una?.sheetOverlay, props._sheetOverlay?.class)"
/>
<DialogContent
v-bind="{ ...forwarded, ...$attrs }"
:sheet
:class="cn('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 { DialogTitle } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '../../utils'
const props = defineProps<NSheetTitleProps>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<DialogTitle
: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 { DialogDescription } from 'reka-ui'
import { computed } from 'vue'
import { cn } from '../../utils'
const props = defineProps<NSheetDescriptionProps>()
const delegatedProps = computed(() => {
const { class: _, ...delegated } = props
return delegated
})
</script>
<template>
<DialogDescription
:class="cn('sheet-description', props.una?.sheetDescription, props.class)"
v-bind="delegatedProps"
>
<slot />
</DialogDescription>
</template>
a
<script setup lang="ts">
import type { NSheetHeaderProps } from '../../types'
import { cn } from '../../utils'
const props = defineProps<NSheetHeaderProps>()
</script>
<template>
<div
: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
: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
as-child
>
<slot>
<Button
v-bind="props"
/>
</slot>
</DialogClose>
</template>