Skip to content
edmi-ui

Sheet

A dialog that slides in from an edge. Use for filters, details and settings that relate to the page behind.

StackBlitz ↗StackBlitz ↗StackBlitz ↗
Terminal window
npx shadcn@latest add @edmi-ui/sheet
Terminal window
pnpm dlx shadcn@latest add @edmi-ui/sheet
Terminal window
yarn dlx shadcn@latest add @edmi-ui/sheet
Terminal window
bunx shadcn@latest add @edmi-ui/sheet

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-vue@latest add @edmi-ui/sheet
Terminal window
pnpm dlx shadcn-vue@latest add @edmi-ui/sheet
Terminal window
yarn dlx shadcn-vue@latest add @edmi-ui/sheet
Terminal window
bunx shadcn-vue@latest add @edmi-ui/sheet

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

Terminal window
npx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/sheet.json
Terminal window
pnpm dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/sheet.json
Terminal window
yarn dlx shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/sheet.json
Terminal window
bunx --bun shadcn-svelte@latest add https://viandwi24.github.io/edmi-ui/r/svelte/sheet.json

Pick your package manager above; the choice is remembered. Add the @edmi-ui registry first, see Getting started.

import { Button } from "@/components/ui/button";
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@/components/ui/sheet";
<Sheet>
<SheetTrigger render={<Button variant="outline" />}>Filters</SheetTrigger>
<SheetContent side="right">
<SheetHeader>
<SheetTitle>Filters</SheetTitle>
<SheetDescription>Narrow the index list.</SheetDescription>
</SheetHeader>
<div className="grid gap-2 px-[22px] text-[13px]">
<label className="flex items-center gap-2" htmlFor="sheet-xstocks">
<input id="sheet-xstocks" type="checkbox" defaultChecked />
xStocks
</label>
<label className="flex items-center gap-2" htmlFor="sheet-ipo">
<input id="sheet-ipo" type="checkbox" defaultChecked />
Pre-IPO
</label>
</div>
<SheetFooter>
<Button>Apply</Button>
<SheetClose render={<Button variant="outline" />}>Reset</SheetClose>
</SheetFooter>
</SheetContent>
</Sheet>
<script setup lang="ts">
import { Button } from "@/components/ui/button";
import { Sheet, SheetTrigger } from "@/components/ui/sheet";
</script>
<template>
<Sheet>
<SheetTrigger as-child>
<Button variant="outline">Filters</Button>
</SheetTrigger>
<!-- … -->
</Sheet>
</template>
<script lang="ts">
import * as Sheet from "$lib/components/ui/sheet";
import { Button } from "$lib/components/ui/button";
</script>
<Sheet.Root>
<Sheet.Trigger>
{#snippet child({ props })}
<Button variant="outline" {...props}>Filters</Button>
{/snippet}
</Sheet.Trigger>
<!-- … -->
</Sheet.Root>
registry/ui/sheet.tsx
import { Dialog as SheetPrimitive } from "@base-ui/react/dialog";
import { cn } from "cn";
import type * as React from "react";
import { XIcon } from "@phosphor-icons/react";
import { Button } from "@/registry/edmi/ui/button";
function Sheet({ ...props }: SheetPrimitive.Root.Props) {
return <SheetPrimitive.Root data-slot="sheet" {...props} />;
}
function SheetTrigger({ ...props }: SheetPrimitive.Trigger.Props) {
return <SheetPrimitive.Trigger data-slot="sheet-trigger" {...props} />;
}
function SheetClose({ ...props }: SheetPrimitive.Close.Props) {
return <SheetPrimitive.Close data-slot="sheet-close" {...props} />;
}
function SheetPortal({ ...props }: SheetPrimitive.Portal.Props) {
return <SheetPrimitive.Portal data-slot="sheet-portal" {...props} />;
}
function SheetOverlay({ className, ...props }: SheetPrimitive.Backdrop.Props) {
return (
<SheetPrimitive.Backdrop
data-slot="sheet-overlay"
className={cn(
"fixed inset-0 z-50 bg-overlay transition-opacity duration-150 data-[ending-style]:opacity-0 data-[starting-style]:opacity-0",
className,
)}
{...props}
/>
);
}
function SheetContent({
className,
children,
side = "right",
showCloseButton = true,
...props
}: SheetPrimitive.Popup.Props & {
side?: "top" | "right" | "bottom" | "left";
showCloseButton?: boolean;
}) {
return (
<SheetPortal>
<SheetOverlay />
<SheetPrimitive.Popup
data-slot="sheet-content"
data-side={side}
className={cn(
// Flat: 1px border on the inner edge.
"fixed z-50 flex flex-col gap-4 bg-popover text-sm text-popover-foreground outline-none transition-transform duration-200 ease-in-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=bottom]:data-[ending-style]:translate-y-full data-[side=bottom]:data-[starting-style]:translate-y-full data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=left]:data-[ending-style]:-translate-x-full data-[side=left]:data-[starting-style]:-translate-x-full data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=right]:data-[ending-style]:translate-x-full data-[side=right]:data-[starting-style]:translate-x-full data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=top]:data-[ending-style]:-translate-y-full data-[side=top]:data-[starting-style]:-translate-y-full data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm",
className,
)}
{...props}
>
{children}
{showCloseButton && (
<SheetPrimitive.Close
data-slot="sheet-close"
render={
<Button
variant="ghost"
className="absolute top-3.5 right-3.5"
size="icon-xs"
/>
}
>
<XIcon
/>
<span className="sr-only">Close</span>
</SheetPrimitive.Close>
)}
</SheetPrimitive.Popup>
</SheetPortal>
);
}
function SheetHeader({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-header"
className={cn("flex flex-col gap-1 p-[22px] pb-0", className)}
{...props}
/>
);
}
function SheetFooter({ className, ...props }: React.ComponentProps<"div">) {
return (
<div
data-slot="sheet-footer"
className={cn("mt-auto flex flex-col gap-2 p-[22px] pt-0", className)}
{...props}
/>
);
}
function SheetTitle({ className, ...props }: SheetPrimitive.Title.Props) {
return (
<SheetPrimitive.Title
data-slot="sheet-title"
className={cn(
"text-base leading-snug font-semibold tracking-[-0.2px] text-foreground",
className,
)}
{...props}
/>
);
}
function SheetDescription({
className,
...props
}: SheetPrimitive.Description.Props) {
return (
<SheetPrimitive.Description
data-slot="sheet-description"
className={cn("text-[13px] text-muted-foreground", className)}
{...props}
/>
);
}
export {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
};
registry/ui/sheet/Sheet.vue
<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-slot="sheet"
v-bind="forwarded"
>
<slot v-bind="slotProps" />
</DialogRoot>
</template>
registry/ui/sheet/SheetClose.vue
<script setup lang="ts">
import type { DialogCloseProps } from 'reka-ui'
import { DialogClose } from 'reka-ui'
const props = defineProps<DialogCloseProps>()
</script>
<template>
<DialogClose
data-slot="sheet-close"
v-bind="props"
>
<slot />
</DialogClose>
</template>
registry/ui/sheet/SheetContent.vue
<script setup lang="ts">
import type { DialogContentEmits, DialogContentProps } from 'reka-ui'
import type { HTMLAttributes } from 'vue'
import { X } from '@lucide/vue'
import { reactiveOmit } from '@vueuse/core'
import {
DialogClose,
DialogContent,
DialogPortal,
useForwardPropsEmits,
} from 'reka-ui'
import { Button } from '@/registry/edmi/ui/button'
import { cn } from '@/registry/edmi/lib/utils'
import SheetOverlay from './SheetOverlay.vue'
defineOptions({
inheritAttrs: false,
})
const props = withDefaults(defineProps<DialogContentProps & {
class?: HTMLAttributes['class']
side?: 'top' | 'right' | 'bottom' | 'left'
showCloseButton?: boolean
}>(), {
side: 'right',
showCloseButton: true,
})
const emits = defineEmits<DialogContentEmits>()
const delegatedProps = reactiveOmit(props, 'class', 'side', 'showCloseButton')
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DialogPortal>
<SheetOverlay />
<DialogContent
data-slot="sheet-content"
:data-side="side"
:class="cn(
// Flat: 1px border on the inner edge.
'fixed z-50 flex flex-col gap-4 border-border bg-popover text-sm text-popover-foreground outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-200 data-[state=open]:duration-300 data-[state=closed]:ease-in data-[state=open]:ease-out data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=bottom]:data-[state=closed]:slide-out-to-bottom data-[side=bottom]:data-[state=open]:slide-in-from-bottom data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=left]:data-[state=closed]:slide-out-to-left data-[side=left]:data-[state=open]:slide-in-from-left data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=right]:data-[state=closed]:slide-out-to-right data-[side=right]:data-[state=open]:slide-in-from-right data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=top]:data-[state=closed]:slide-out-to-top data-[side=top]:data-[state=open]:slide-in-from-top data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm',
props.class,
)"
v-bind="{ ...$attrs, ...forwarded }"
>
<slot />
<DialogClose
v-if="showCloseButton"
as-child
>
<Button
data-slot="sheet-close"
variant="ghost"
size="icon-xs"
class="absolute top-3.5 right-3.5"
>
<X />
<span class="sr-only">Close</span>
</Button>
</DialogClose>
</DialogContent>
</DialogPortal>
</template>
registry/ui/sheet/SheetDescription.vue
<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 '@/registry/edmi/lib/utils'
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<DialogDescription
data-slot="sheet-description"
v-bind="delegatedProps"
:class="cn('text-[13px] text-muted-foreground', props.class)"
>
<slot />
</DialogDescription>
</template>
registry/ui/sheet/SheetFooter.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-slot="sheet-footer"
:class="cn('mt-auto flex flex-col gap-2 p-[22px] pt-0', props.class)"
>
<slot />
</div>
</template>
registry/ui/sheet/SheetHeader.vue
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '@/registry/edmi/lib/utils'
const props = defineProps<{
class?: HTMLAttributes['class']
}>()
</script>
<template>
<div
data-slot="sheet-header"
:class="cn('flex flex-col gap-1 p-[22px] pb-0', props.class)"
>
<slot />
</div>
</template>
registry/ui/sheet/SheetOverlay.vue
<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 '@/registry/edmi/lib/utils'
const props = defineProps<DialogOverlayProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<DialogOverlay
data-slot="sheet-overlay"
v-bind="delegatedProps"
:class="cn('fixed inset-0 z-50 bg-overlay data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0', props.class)"
>
<slot />
</DialogOverlay>
</template>
registry/ui/sheet/SheetTitle.vue
<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 '@/registry/edmi/lib/utils'
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes['class'] }>()
const delegatedProps = reactiveOmit(props, 'class')
</script>
<template>
<DialogTitle
data-slot="sheet-title"
v-bind="delegatedProps"
:class="cn('text-base leading-snug font-semibold tracking-[-0.2px] text-foreground', props.class)"
>
<slot />
</DialogTitle>
</template>
registry/ui/sheet/SheetTrigger.vue
<script setup lang="ts">
import type { DialogTriggerProps } from 'reka-ui'
import { DialogTrigger } from 'reka-ui'
const props = defineProps<DialogTriggerProps>()
</script>
<template>
<DialogTrigger
data-slot="sheet-trigger"
v-bind="props"
>
<slot />
</DialogTrigger>
</template>
registry/ui/sheet/index.ts
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 SheetOverlay } from "./SheetOverlay.vue";
export { default as SheetTitle } from "./SheetTitle.vue";
export { default as SheetTrigger } from "./SheetTrigger.vue";
src/lib/registry/ui/sheet/sheet-close.svelte
<script lang="ts">
import { Dialog as Primitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: Primitive.CloseProps = $props();
</script>
<Primitive.Close bind:ref data-slot="sheet-close" {...restProps} />
src/lib/registry/ui/sheet/sheet-content.svelte
<script lang="ts" module>
export type Side = "top" | "right" | "bottom" | "left";
</script>
<script lang="ts">
import { Dialog as SheetPrimitive } from "bits-ui";
import XIcon from "phosphor-svelte/lib/X";
import { Button } from "$lib/registry/ui/button/index.js";
import { cn, type WithoutChildrenOrChild } from "$lib/utils.js";
import SheetOverlay from "./sheet-overlay.svelte";
import SheetPortal from "./sheet-portal.svelte";
import type { Snippet } from "svelte";
import type { ComponentProps } from "svelte";
let {
ref = $bindable(null),
class: className,
side = "right",
showCloseButton = true,
portalProps,
children,
...restProps
}: WithoutChildrenOrChild<SheetPrimitive.ContentProps> & {
portalProps?: WithoutChildrenOrChild<ComponentProps<typeof SheetPortal>>;
side?: Side;
showCloseButton?: boolean;
children: Snippet;
} = $props();
</script>
<SheetPortal {...portalProps}>
<SheetOverlay />
<SheetPrimitive.Content
bind:ref
data-slot="sheet-content"
data-side={side}
class={cn(
// Flat: a 1px border on the inner edge.
"fixed z-50 flex flex-col gap-4 bg-popover text-sm text-popover-foreground outline-none transition ease-in-out data-[state=open]:animate-in data-[state=open]:duration-200 data-[state=closed]:animate-out data-[state=closed]:duration-150",
"data-[side=bottom]:inset-x-0 data-[side=bottom]:bottom-0 data-[side=bottom]:h-auto data-[side=bottom]:border-t data-[side=bottom]:data-[state=open]:slide-in-from-bottom data-[side=bottom]:data-[state=closed]:slide-out-to-bottom",
"data-[side=left]:inset-y-0 data-[side=left]:left-0 data-[side=left]:h-full data-[side=left]:w-3/4 data-[side=left]:border-r data-[side=left]:data-[state=open]:slide-in-from-left data-[side=left]:data-[state=closed]:slide-out-to-left data-[side=left]:sm:max-w-sm",
"data-[side=right]:inset-y-0 data-[side=right]:right-0 data-[side=right]:h-full data-[side=right]:w-3/4 data-[side=right]:border-l data-[side=right]:data-[state=open]:slide-in-from-right data-[side=right]:data-[state=closed]:slide-out-to-right data-[side=right]:sm:max-w-sm",
"data-[side=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=top]:data-[state=open]:slide-in-from-top data-[side=top]:data-[state=closed]:slide-out-to-top",
className
)}
{...restProps}
>
{@render children?.()}
{#if showCloseButton}
<SheetPrimitive.Close data-slot="sheet-close">
{#snippet child({ props })}
<Button variant="ghost" class="absolute top-3.5 right-3.5" size="icon-xs" {...props}>
<XIcon
/>
<span class="sr-only">Close</span>
</Button>
{/snippet}
</SheetPrimitive.Close>
{/if}
</SheetPrimitive.Content>
</SheetPortal>
src/lib/registry/ui/sheet/sheet-description.svelte
<script lang="ts">
import { Dialog as Primitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: Primitive.DescriptionProps = $props();
</script>
<Primitive.Description
bind:ref
data-slot="sheet-description"
class={cn("text-[13px] text-muted-foreground", className)}
{...restProps}
/>
src/lib/registry/ui/sheet/sheet-footer.svelte
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="sheet-footer"
class={cn("mt-auto flex flex-col gap-2 p-[22px] pt-0", className)}
{...restProps}
>
{@render children?.()}
</div>
src/lib/registry/ui/sheet/sheet-header.svelte
<script lang="ts">
import { cn, type WithElementRef } from "$lib/utils.js";
import type { HTMLAttributes } from "svelte/elements";
let {
ref = $bindable(null),
class: className,
children,
...restProps
}: WithElementRef<HTMLAttributes<HTMLDivElement>> = $props();
</script>
<div
bind:this={ref}
data-slot="sheet-header"
class={cn("flex flex-col gap-1 p-[22px] pb-0", className)}
{...restProps}
>
{@render children?.()}
</div>
src/lib/registry/ui/sheet/sheet-overlay.svelte
<script lang="ts">
import { Dialog as Primitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: Primitive.OverlayProps = $props();
</script>
<Primitive.Overlay
bind:ref
data-slot="sheet-overlay"
class={cn("fixed inset-0 z-50 bg-overlay data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0", className)}
{...restProps}
/>
src/lib/registry/ui/sheet/sheet-portal.svelte
<script lang="ts">
import { Dialog as Primitive } from "bits-ui";
let { ...restProps }: Primitive.PortalProps = $props();
</script>
<Primitive.Portal {...restProps} />
src/lib/registry/ui/sheet/sheet-title.svelte
<script lang="ts">
import { Dialog as Primitive } from "bits-ui";
import { cn } from "$lib/utils.js";
let {
ref = $bindable(null),
class: className,
...restProps
}: Primitive.TitleProps = $props();
</script>
<Primitive.Title
bind:ref
data-slot="sheet-title"
class={cn("text-base leading-snug font-semibold tracking-[-0.2px] text-foreground", className)}
{...restProps}
/>
src/lib/registry/ui/sheet/sheet-trigger.svelte
<script lang="ts">
import { Dialog as Primitive } from "bits-ui";
let { ref = $bindable(null), ...restProps }: Primitive.TriggerProps = $props();
</script>
<Primitive.Trigger bind:ref data-slot="sheet-trigger" {...restProps} />
src/lib/registry/ui/sheet/sheet.svelte
<script lang="ts">
import { Dialog as Primitive } from "bits-ui";
let { open = $bindable(false), ...restProps }: Primitive.RootProps = $props();
</script>
<Primitive.Root bind:open {...restProps} />
src/lib/registry/ui/sheet/index.ts
import Root from "./sheet.svelte";
import Close from "./sheet-close.svelte";
import Content from "./sheet-content.svelte";
import Description from "./sheet-description.svelte";
import Footer from "./sheet-footer.svelte";
import Header from "./sheet-header.svelte";
import Overlay from "./sheet-overlay.svelte";
import Portal from "./sheet-portal.svelte";
import Title from "./sheet-title.svelte";
import Trigger from "./sheet-trigger.svelte";
export {
Close,
Close as SheetClose,
Content,
Content as SheetContent,
Description,
Description as SheetDescription,
Footer,
Footer as SheetFooter,
Header,
Header as SheetHeader,
Overlay,
Overlay as SheetOverlay,
Portal,
Portal as SheetPortal,
Root,
//
Root as Sheet,
Title,
Title as SheetTitle,
Trigger,
Trigger as SheetTrigger,
};
Prop Type Default
SheetContent side top | right | bottom | left right
SheetContent showCloseButton boolean true

Parts: Sheet, SheetTrigger, SheetContent, SheetHeader, SheetTitle, SheetDescription, SheetFooter, SheetClose.

  • Hard 4px strong lip on the edge facing the page; --overlay scrim.