Dialog

A window overlaid on either the primary window or another dialog window, rendering the content underneath inert.

With Form
Scrollable Content
With Sticky Footer
No Close Button
Chat Settings

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/dialog.

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

src/lib/components/ui/dialog/DialogClose.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="dialog-close" {children} {...props} />
src/lib/components/ui/dialog/DialogContent.svelte
<script lang="ts">
  import type { ComponentProps } from 'svelte';
  import { Dialog as Primitive } from '@sveltery/base/dialog';
  import { cn } from 'cn';
  import DialogPortal from './DialogPortal.svelte';
  import DialogOverlay from './DialogOverlay.svelte';
  import { Button } from '../button/index.js';
  import XIcon from '@lucide/svelte/icons/x';
  let { children, class: classProp, showCloseButton = true, ...props }: ComponentProps<typeof Primitive.Popup> & { showCloseButton?: boolean } = $props();
</script>
<DialogPortal>
  <DialogOverlay />
  <Primitive.Popup data-slot="dialog-content" {...props} class={cn('bg-popover text-popover-foreground data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 data-closed:zoom-out-95 data-open:zoom-in-95 ring-foreground/10 grid max-w-[calc(100%-2rem)] gap-4 rounded-xl p-4 text-sm ring-1 duration-100 sm:max-w-sm fixed top-1/2 left-1/2 z-50 w-full -translate-x-1/2 -translate-y-1/2 outline-none', classProp)}>
    {@render children?.()}
    {#if showCloseButton}
      <Primitive.Close data-slot="dialog-close">
        {#snippet render(buttonProps, _state, content)}
          <Button variant="ghost" class="absolute top-2 right-2" size="icon-sm" {...buttonProps}>{@render content?.()}</Button>
        {/snippet}
        <XIcon />
        <span class="sr-only">Close</span>
      </Primitive.Close>
    {/if}
  </Primitive.Popup>
</DialogPortal>
src/lib/components/ui/dialog/DialogDescription.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="dialog-description" {children} {...props} class={cn('text-muted-foreground *:[a]:hover:text-foreground text-sm *:[a]:underline *:[a]:underline-offset-3', classProp)} />
src/lib/components/ui/dialog/DialogFooter.svelte
<script lang="ts">
  import type { HTMLAttributes } from 'svelte/elements';
  import { Dialog as Primitive } from '@sveltery/base/dialog';
  import { cn } from 'cn';
  import { Button } from '../button/index.js';
  let { children, class: classProp, showCloseButton = false, ...props }: HTMLAttributes<HTMLDivElement> & { showCloseButton?: boolean } = $props();
</script>
<div data-slot="dialog-footer" {...props} class={cn('bg-muted/50 -mx-4 -mb-4 rounded-b-xl border-t p-4 flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', classProp)}>
  {@render children?.()}
  {#if showCloseButton}
    <Primitive.Close>
      {#snippet render(buttonProps, _state, content)}
        <Button variant="outline" {...buttonProps}>{@render content?.()}</Button>
      {/snippet}
      Close
    </Primitive.Close>
  {/if}
</div>
src/lib/components/ui/dialog/DialogHeader.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="dialog-header" {...props} class={cn('gap-2 flex flex-col', classProp)}>
  {@render children?.()}
</div>
src/lib/components/ui/dialog/DialogOverlay.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="dialog-overlay" {children} {...props} class={cn('data-open:animate-in data-closed:animate-out data-closed:fade-out-0 data-open:fade-in-0 bg-black/10 duration-100 supports-backdrop-filter:backdrop-blur-xs fixed inset-0 isolate z-50', classProp)} />
src/lib/components/ui/dialog/DialogPortal.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="dialog-portal" {...props}>
  {@render children?.()}
</Primitive.Portal>
src/lib/components/ui/dialog/DialogTitle.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="dialog-title" {children} {...props} class={cn('text-base leading-none font-medium', classProp)} />
src/lib/components/ui/dialog/DialogTrigger.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="dialog-trigger" {children} {...props} />
src/lib/components/ui/dialog/index.ts
import { Dialog as Primitive } from '@sveltery/base/dialog';

export const Dialog = Primitive.Root as typeof Primitive.Root;
export { default as DialogTrigger } from './DialogTrigger.svelte';
export { default as DialogPortal } from './DialogPortal.svelte';
export { default as DialogClose } from './DialogClose.svelte';
export { default as DialogOverlay } from './DialogOverlay.svelte';
export { default as DialogContent } from './DialogContent.svelte';
export { default as DialogHeader } from './DialogHeader.svelte';
export { default as DialogFooter } from './DialogFooter.svelte';
export { default as DialogTitle } from './DialogTitle.svelte';
export { default as DialogDescription } from './DialogDescription.svelte';

Update the import paths

Change the imports to match your project setup.

Usage

<script lang="ts">
  import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '$lib/components/ui/dialog';
</script>
<Dialog>
  <DialogTrigger>Open</DialogTrigger>
  <DialogContent>
    <DialogHeader>
      <DialogTitle>Are you absolutely sure?</DialogTitle>
      <DialogDescription>This action cannot be undone. It permanently deletes your account.</DialogDescription>
    </DialogHeader>
  </DialogContent>
</Dialog>

API reference

Dialog 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.