Extends the Dialog component to display content that complements the main content of the screen.

With Form
No Close Button
Sides

Installation

Install the dependencies

pnpm add cn class-variance-authority github:sveltery/base

Copy the component

Copy the following files into src/lib/components/ui/sheet.

It imports the button component, so copy its folder too.

src/lib/components/ui/sheet/SheetClose.svelte
<script lang="ts">
  import type { ComponentProps } from 'svelte';
  import { Dialog as Primitive } from '@sveltery/base/dialog';
  let { children, ...props }: ComponentProps<typeof Primitive.Close> = $props();
</script>
<Primitive.Close data-slot="sheet-close" {children} {...props} />
src/lib/components/ui/sheet/SheetContent.svelte
<script lang="ts">
  import type { ComponentProps } from 'svelte';
  import { Dialog as Primitive } from '@sveltery/base/dialog';
  import { cn } from 'cn';
  import SheetPortal from './SheetPortal.svelte';
  import SheetOverlay from './SheetOverlay.svelte';
  import { Button } from '../button/index.js';
  import XIcon from '@lucide/svelte/icons/x';
  let { children, class: classProp, side = 'right', showCloseButton = true, ...props }: ComponentProps<typeof Primitive.Popup> & { side?: 'top' | 'right' | 'bottom' | 'left'; showCloseButton?: boolean } = $props();
</script>
<SheetPortal>
  <SheetOverlay />
  <Primitive.Popup
    data-slot="sheet-content"
    data-side={side}
    {...props}
    class={cn(
      'bg-popover text-popover-foreground fixed z-50 flex flex-col gap-4 bg-clip-padding text-sm shadow-lg transition 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=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=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=top]:inset-x-0 data-[side=top]:top-0 data-[side=top]:h-auto data-[side=top]:border-b data-[side=left]:sm:max-w-sm data-[side=right]:sm:max-w-sm data-ending-style:opacity-0 data-starting-style:opacity-0 data-[side=bottom]:data-ending-style:translate-y-[2.5rem] data-[side=bottom]:data-starting-style:translate-y-[2.5rem] data-[side=left]:data-ending-style:translate-x-[-2.5rem] data-[side=left]:data-starting-style:translate-x-[-2.5rem] data-[side=right]:data-ending-style:translate-x-[2.5rem] data-[side=right]:data-starting-style:translate-x-[2.5rem] data-[side=top]:data-ending-style:translate-y-[-2.5rem] data-[side=top]:data-starting-style:translate-y-[-2.5rem]',
      classProp,
    )}
  >
    {@render children?.()}
    {#if showCloseButton}
      <Primitive.Close data-slot="sheet-close">
        {#snippet render(buttonProps, _state, content)}
          <Button variant="ghost" class="absolute top-3 right-3" size="icon-sm" {...buttonProps}>{@render content?.()}</Button>
        {/snippet}
        <XIcon />
        <span class="sr-only">Close</span>
      </Primitive.Close>
    {/if}
  </Primitive.Popup>
</SheetPortal>
src/lib/components/ui/sheet/SheetDescription.svelte
<script lang="ts">
  import type { ComponentProps } from 'svelte';
  import { Dialog as Primitive } from '@sveltery/base/dialog';
  import { cn } from 'cn';
  let { children, class: classProp, ...props }: ComponentProps<typeof Primitive.Description> = $props();
</script>
<Primitive.Description data-slot="sheet-description" {children} {...props} class={cn('text-muted-foreground text-sm', classProp)} />
src/lib/components/ui/sheet/SheetFooter.svelte
<script lang="ts">
  import type { HTMLAttributes } from 'svelte/elements';
  import { cn } from 'cn';
  let { children, class: classProp, ...props }: HTMLAttributes<HTMLDivElement> = $props();
</script>
<div data-slot="sheet-footer" {...props} class={cn('gap-2 p-4 mt-auto flex flex-col', classProp)}>
  {@render children?.()}
</div>
src/lib/components/ui/sheet/SheetHeader.svelte
<script lang="ts">
  import type { HTMLAttributes } from 'svelte/elements';
  import { cn } from 'cn';
  let { children, class: classProp, ...props }: HTMLAttributes<HTMLDivElement> = $props();
</script>
<div data-slot="sheet-header" {...props} class={cn('gap-0.5 p-4 flex flex-col', classProp)}>
  {@render children?.()}
</div>
src/lib/components/ui/sheet/SheetOverlay.svelte
<script lang="ts">
  import type { ComponentProps } from 'svelte';
  import { Dialog as Primitive } from '@sveltery/base/dialog';
  import { cn } from 'cn';
  let { children, class: classProp, ...props }: ComponentProps<typeof Primitive.Backdrop> = $props();
</script>
<Primitive.Backdrop data-slot="sheet-overlay" {children} {...props} class={cn('bg-black/10 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 z-50 transition-opacity duration-150 data-ending-style:opacity-0 data-starting-style:opacity-0', classProp)} />
src/lib/components/ui/sheet/SheetPortal.svelte
<script lang="ts">
  import type { ComponentProps } from 'svelte';
  import { Dialog as Primitive } from '@sveltery/base/dialog';
  let { children, ...props }: ComponentProps<typeof Primitive.Portal> = $props();
</script>
<Primitive.Portal data-slot="sheet-portal" {...props}>
  {@render children?.()}
</Primitive.Portal>
src/lib/components/ui/sheet/SheetTitle.svelte
<script lang="ts">
  import type { ComponentProps } from 'svelte';
  import { Dialog as Primitive } from '@sveltery/base/dialog';
  import { cn } from 'cn';
  let { children, class: classProp, ...props }: ComponentProps<typeof Primitive.Title> = $props();
</script>
<Primitive.Title data-slot="sheet-title" {children} {...props} class={cn('text-foreground text-base font-medium', classProp)} />
src/lib/components/ui/sheet/SheetTrigger.svelte
<script lang="ts">
  import type { ComponentProps } from 'svelte';
  import { Dialog as Primitive } from '@sveltery/base/dialog';
  let { children, ...props }: ComponentProps<typeof Primitive.Trigger> = $props();
</script>
<Primitive.Trigger data-slot="sheet-trigger" {children} {...props} />
src/lib/components/ui/sheet/index.ts
import { Dialog as Primitive } from '@sveltery/base/dialog';

export const Sheet = Primitive.Root as typeof Primitive.Root;
export { default as SheetTrigger } from './SheetTrigger.svelte';
export { default as SheetClose } from './SheetClose.svelte';
export { default as SheetContent } from './SheetContent.svelte';
export { default as SheetHeader } from './SheetHeader.svelte';
export { default as SheetFooter } from './SheetFooter.svelte';
export { default as SheetTitle } from './SheetTitle.svelte';
export { default as SheetDescription } from './SheetDescription.svelte';

Update the import paths

Change the imports to match your project setup.

Usage

<script lang="ts">
  import { Sheet, SheetContent, SheetDescription, SheetHeader, SheetTitle, SheetTrigger } from '$lib/components/ui/sheet';
  let open = $state(false);
</script>
<Sheet bind:open>
  <SheetTrigger>Open</SheetTrigger>
  <SheetContent side="right">
    <SheetHeader>
      <SheetTitle>Are you absolutely sure?</SheetTitle>
      <SheetDescription>This action cannot be undone. It permanently deletes your account.</SheetDescription>
    </SheetHeader>
  </SheetContent>
</Sheet>

SheetContent takes side (top, right, bottom or left, default right) and showCloseButton (default true).

API reference

Sheet is styled on top of the Sveltery Base Dialog primitive, which owns its behavior, focus handling and accessibility. See the Base UI Dialog documentation for the behavior it ports.