UIPackage

Sheet

Vue overlay
Edit on GitHub

Side-mounted modal that slides in from the top, right, bottom, or left edge. Use for filter panels, edit drawers, and mobile menus.

Also available for React ->

Installation

$ npx shadcn-vue@latest add https://uipkge.dev/r/vue/sheet.json

Or with the named registry: npx shadcn-vue@latest add @uipkge/sheet

Examples

Dependencies

Used by

Files (10)

  • app/components/ui/sheet/Sheet.vue 0.4 kB
    <script setup lang="ts">
    import type { DialogRootEmits, DialogRootProps } from 'reka-ui'
    import { DialogRoot, useForwardPropsEmits } from 'reka-ui'
    
    const props = defineProps<DialogRootProps>()
    const emits = defineEmits<DialogRootEmits>()
    
    const forwarded = useForwardPropsEmits(props, emits)
    </script>
    
    <template>
      <DialogRoot v-slot="slotProps" data-uipkge data-slot="sheet" v-bind="forwarded">
        <slot v-bind="slotProps" />
      </DialogRoot>
    </template>
  • app/components/ui/sheet/SheetClose.vue 0.3 kB
    <script setup lang="ts">
    import type { DialogCloseProps } from 'reka-ui'
    import { DialogClose } from 'reka-ui'
    
    const props = defineProps<DialogCloseProps>()
    </script>
    
    <template>
      <DialogClose data-uipkge data-slot="sheet-close" v-bind="props">
        <slot />
      </DialogClose>
    </template>
  • app/components/ui/sheet/SheetContent.vue 3 kB
    <script setup lang="ts">
    import type { DialogContentEmits, DialogContentProps } from 'reka-ui'
    import type { HTMLAttributes } from 'vue'
    import { reactiveOmit } from '@vueuse/core'
    import { X } from 'lucide-vue-next'
    import { DialogClose, DialogContent, DialogPortal, useForwardPropsEmits } from 'reka-ui'
    import { cn } from '@/lib/utils'
    import SheetOverlay from './SheetOverlay.vue'
    
    // SFC compiler quirk: when the extension type is a *named* interface
    // in an intersection (`DialogContentProps & ExtraProps`), the runtime
    // extractor occasionally drops fields from the named interface --
    // `side` then warns "accessed during render but not defined on
    // instance" every time the FilterSheet mounts. Inlining the literal
    // keeps the extractor happy. DialogContentProps stays in the
    // intersection so reka-ui's typed props (asChild, trapFocus, etc.)
    // flow through useForwardPropsEmits below.
    defineOptions({
      inheritAttrs: false,
    })
    
    const props = withDefaults(
      defineProps<
        DialogContentProps & {
          class?: HTMLAttributes['class']
          side?: 'top' | 'right' | 'bottom' | 'left'
        }
      >(),
      { side: 'right' },
    )
    const emits = defineEmits<DialogContentEmits>()
    
    const delegatedProps = reactiveOmit(props, 'class', 'side')
    
    const forwarded = useForwardPropsEmits(delegatedProps, emits)
    </script>
    
    <template>
      <DialogPortal>
        <SheetOverlay />
        <DialogContent
          data-uipkge
          data-slot="sheet-content"
          :class="
            cn(
              'bg-background motion-safe:data-[state=open]:animate-in motion-safe:data-[state=closed]:animate-out fixed z-50 flex flex-col gap-4 shadow-lg transition ease-in-out motion-safe:data-[state=closed]:duration-200 motion-safe:data-[state=open]:duration-300',
              side === 'right' &&
                'motion-safe:data-[state=closed]:slide-out-to-right motion-safe:data-[state=open]:slide-in-from-right inset-y-0 right-0 h-full w-3/4 border-l sm:max-w-sm',
              side === 'left' &&
                'motion-safe:data-[state=closed]:slide-out-to-left motion-safe:data-[state=open]:slide-in-from-left inset-y-0 left-0 h-full w-3/4 border-r sm:max-w-sm',
              side === 'top' &&
                'motion-safe:data-[state=closed]:slide-out-to-top motion-safe:data-[state=open]:slide-in-from-top inset-x-0 top-0 h-auto border-b',
              side === 'bottom' &&
                'motion-safe:data-[state=closed]:slide-out-to-bottom motion-safe:data-[state=open]:slide-in-from-bottom inset-x-0 bottom-0 h-auto border-t',
              props.class,
            )
          "
          v-bind="{ ...$attrs, ...forwarded }"
        >
          <slot />
    
          <DialogClose
            class="ring-offset-background focus:ring-ring data-[state=open]:bg-secondary absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none"
          >
            <X class="size-4" aria-hidden="true" />
            <span class="sr-only">Close</span>
          </DialogClose>
        </DialogContent>
      </DialogPortal>
    </template>
  • app/components/ui/sheet/SheetDescription.vue 0.6 kB
    <script setup lang="ts">
    import type { DialogDescriptionProps } from 'reka-ui'
    import type { HTMLAttributes } from 'vue'
    import { reactiveOmit } from '@vueuse/core'
    import { DialogDescription } from 'reka-ui'
    import { cn } from '@/lib/utils'
    
    const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
    
    const delegatedProps = reactiveOmit(props, 'class')
    </script>
    
    <template>
      <DialogDescription
        data-uipkge
        data-slot="sheet-description"
        :class="cn('text-muted-foreground text-sm', props.class)"
        v-bind="delegatedProps"
      >
        <slot />
      </DialogDescription>
    </template>
  • app/components/ui/sheet/SheetFooter.vue 0.3 kB
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue'
    import { cn } from '@/lib/utils'
    
    const props = defineProps<{ class?: HTMLAttributes['class'] }>()
    </script>
    
    <template>
      <div data-uipkge data-slot="sheet-footer" :class="cn('mt-auto flex flex-col gap-2 p-4', props.class)">
        <slot />
      </div>
    </template>
  • app/components/ui/sheet/SheetHeader.vue 0.3 kB
    <script setup lang="ts">
    import type { HTMLAttributes } from 'vue'
    import { cn } from '@/lib/utils'
    
    const props = defineProps<{ class?: HTMLAttributes['class'] }>()
    </script>
    
    <template>
      <div data-uipkge data-slot="sheet-header" :class="cn('flex flex-col gap-1.5 p-4', props.class)">
        <slot />
      </div>
    </template>
  • app/components/ui/sheet/SheetOverlay.vue 0.8 kB
    <script setup lang="ts">
    import type { DialogOverlayProps } from 'reka-ui'
    import type { HTMLAttributes } from 'vue'
    import { reactiveOmit } from '@vueuse/core'
    import { DialogOverlay } from 'reka-ui'
    import { cn } from '@/lib/utils'
    
    const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes['class'] }>()
    
    const delegatedProps = reactiveOmit(props, 'class')
    </script>
    
    <template>
      <DialogOverlay
        data-uipkge
        data-slot="sheet-overlay"
        :class="
          cn(
            'motion-safe:data-[state=open]:animate-in motion-safe:data-[state=closed]:animate-out motion-safe:data-[state=closed]:fade-out-0 motion-safe:data-[state=open]:fade-in-0 bg-foreground/50 fixed inset-0 z-50',
            props.class,
          )
        "
        v-bind="delegatedProps"
      >
        <slot />
      </DialogOverlay>
    </template>
  • app/components/ui/sheet/SheetTitle.vue 0.6 kB
    <script setup lang="ts">
    import type { DialogTitleProps } from 'reka-ui'
    import type { HTMLAttributes } from 'vue'
    import { reactiveOmit } from '@vueuse/core'
    import { DialogTitle } from 'reka-ui'
    import { cn } from '@/lib/utils'
    
    const props = defineProps<DialogTitleProps & { class?: HTMLAttributes['class'] }>()
    
    const delegatedProps = reactiveOmit(props, 'class')
    </script>
    
    <template>
      <DialogTitle
        data-uipkge
        data-slot="sheet-title"
        :class="cn('text-foreground font-semibold', props.class)"
        v-bind="delegatedProps"
      >
        <slot />
      </DialogTitle>
    </template>
  • app/components/ui/sheet/SheetTrigger.vue 0.3 kB
    <script setup lang="ts">
    import type { DialogTriggerProps } from 'reka-ui'
    import { DialogTrigger } from 'reka-ui'
    
    const props = defineProps<DialogTriggerProps>()
    </script>
    
    <template>
      <DialogTrigger data-uipkge data-slot="sheet-trigger" v-bind="props">
        <slot />
      </DialogTrigger>
    </template>
  • app/components/ui/sheet/index.ts 0.5 kB
    export { default as Sheet } from './Sheet.vue'
    export { default as SheetClose } from './SheetClose.vue'
    export { default as SheetContent } from './SheetContent.vue'
    export { default as SheetDescription } from './SheetDescription.vue'
    export { default as SheetFooter } from './SheetFooter.vue'
    export { default as SheetHeader } from './SheetHeader.vue'
    export { default as SheetTitle } from './SheetTitle.vue'
    export { default as SheetTrigger } from './SheetTrigger.vue'

Raw manifest: https://uipkge.dev/r/vue/sheet.json