Dialog

A modal window presenting content or seeking user input without navigating away from the current context.

	<script lang="ts">
  import { Dialog, Label, Separator } from "bits-ui";
  import LockKeyOpen from "phosphor-svelte/lib/LockKeyOpen";
  import X from "phosphor-svelte/lib/X";
</script>
 
<Dialog.Root>
  <Dialog.Trigger
    class="inline-flex h-12 items-center
	justify-center whitespace-nowrap rounded-input bg-dark px-[21px]
	text-[15px] font-semibold text-background shadow-mini transition-colors hover:bg-dark/95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background active:scale-98"
  >
    New API key
  </Dialog.Trigger>
  <Dialog.Portal>
    <Dialog.Overlay
      class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
    />
    <Dialog.Content
      class="fixed left-[50%] top-[50%] z-50 w-full max-w-[94%] translate-x-[-50%] translate-y-[-50%] rounded-card-lg border bg-background p-5 shadow-popover outline-none data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:max-w-[490px] md:w-full"
    >
      <Dialog.Title
        class="flex w-full items-center justify-center text-lg font-semibold tracking-tight"
        >Create API key</Dialog.Title
      >
      <Separator.Root class="-mx-5 mb-6 mt-5 block h-px bg-muted" />
      <Dialog.Description class="text-sm text-foreground-alt">
        Create and manage API keys. You can create multiple keys to organize
        your applications.
      </Dialog.Description>
      <div class="flex flex-col items-start gap-1 pb-11 pt-7">
        <Label.Root for="apiKey" class="text-sm font-medium">API Key</Label.Root
        >
        <div class="relative w-full">
          <input
            id="apiKey"
            class="inline-flex h-input w-full items-center rounded-card-sm border border-border-input bg-background px-4 text-sm placeholder:text-foreground-alt/50 hover:border-dark-40 focus:outline-none focus:ring-2 focus:ring-foreground focus:ring-offset-2 focus:ring-offset-background"
            placeholder="secret_api_key"
            type="password"
            autocomplete="off"
          />
          <LockKeyOpen
            class="absolute right-4 top-[14px] size-[22px] text-dark/30"
          />
        </div>
      </div>
      <div class="flex w-full justify-end">
        <Dialog.Close
          class="inline-flex h-input items-center justify-center rounded-input bg-dark px-[50px] text-[15px] font-semibold text-background shadow-mini hover:bg-dark/95 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-dark focus-visible:ring-offset-2 focus-visible:ring-offset-background active:scale-98"
        >
          Save
        </Dialog.Close>
      </div>
      <Dialog.Close
        class="absolute right-5 top-5 rounded-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-foreground focus-visible:ring-offset-2 focus-visible:ring-offset-background active:scale-98"
      >
        <div>
          <X class="size-5 text-foreground" />
          <span class="sr-only">Close</span>
        </div>
      </Dialog.Close>
    </Dialog.Content>
  </Dialog.Portal>
</Dialog.Root>

Overview

The Dialog component in Bits UI provides a flexible and accessible way to create modal dialogs in your Svelte applications. It follows a compound component pattern, allowing for fine-grained control over the dialog's structure and behavior while maintaining accessibility and ease of use.

Key Features

  • Compound Component Structure: Offers a set of subcomponents that work together to create a fully-featured dialog.
  • Accessibility: Built with WAI-ARIA guidelines in mind, ensuring keyboard navigation and screen reader support.
  • Customizable: Each subcomponent can be styled and configured independently.
  • Portal Support: Content can be rendered in a portal, ensuring proper stacking context.
  • Managed Focus: Automatically manages focus, with the option to take control if needed.
  • Flexible State Management: Supports both controlled and uncontrolled state, allowing for full control over the dialog's open state.

Architecture

The Dialog component is composed of several subcomponents, each with a specific role:

  • Root: The main container component that manages the state of the dialog. Provides context for all child components.
  • Trigger: A button that toggles the dialog's open state.
  • Portal: Renders its children in a portal, outside the normal DOM hierarchy.
  • Overlay: A backdrop that sits behind the dialog content.
  • Content: The main container for the dialog's content.
  • Title: Renders the dialog's title.
  • Description: Renders a description or additional context for the dialog.
  • Close: A button that closes the dialog.

Structure

Here's an overview of how the Dialog component is structured in code:

	<script lang="ts">
	import { Dialog } from "bits-ui";
</script>
 
<Dialog.Root>
	<Dialog.Trigger />
	<Dialog.Portal>
		<Dialog.Overlay />
		<Dialog.Content>
			<Dialog.Title />
			<Dialog.Description />
			<Dialog.Close />
		</Dialog.Content>
	</Dialog.Portal>
</Dialog.Root>

Reusable Components

Bits UI provides a comprehensive set of Dialog components that serve as building blocks for creating customized, reusable Dialog implementations. This approach offers flexibility in design while maintaining consistency and accessibility across your application.

Building a Reusable Dialog

The following example demonstrates how to create a versatile, reusable Dialog component using Bits UI building blocks. This implementation showcases the flexibility of the component API by combining props and snippets.

MyDialog.svelte
	<script lang="ts">
	import type { Snippet } from "svelte";
	import { Dialog, type WithoutChild } from "bits-ui";
 
	type Props = Dialog.RootProps & {
		buttonText: string;
		title: Snippet;
		description: Snippet;
		contentProps?: WithoutChild<Dialog.ContentProps>;
		// ...other component props if you wish to pass them
	};
 
	let {
		open = $bindable(false),
		children,
		buttonText,
		contentProps,
		title,
		description,
		...restProps
	}: Props = $props();
</script>
 
<Dialog.Root bind:open {...restProps}>
	<Dialog.Trigger>
		{buttonText}
	</Dialog.Trigger>
	<Dialog.Portal>
		<Dialog.Overlay />
		<Dialog.Content {...contentProps}>
			<Dialog.Title>
				{@render title()}
			</Dialog.Title>
			<Dialog.Description>
				{@render description()}
			</Dialog.Description>
			{@render children?.()}
			<Dialog.Close>Close Dialog</Dialog.Close>
		</Dialog.Content>
	</Dialog.Portal>
</Dialog.Root>

Usage with Inline Snippets

	<script lang="ts">
	import MyDialog from "$lib/components/MyDialog.svelte";
</script>
 
<MyDialog buttonText="Open Dialog">
	{#snippet title()}
		Account settings
	{/snippet}
 
	{#snippet description()}
		Manage your account settings and preferences.
	{/snippet}
 
	<!-- Additional dialog content here... -->
</MyDialog>

Usage with Separate Snippets

	<script lang="ts">
	import MyDialog from "$lib/components/MyDialog.svelte";
</script>
 
{#snippet title()}
	Account settings
{/snippet}
 
{#snippet description()}
	Manage your account settings and preferences.
{/snippet}
 
<MyDialog buttonText="Open Dialog" {title} {description}>
	<!-- Additional dialog content here... -->
</MyDialog>

Best Practices

  • Prop Flexibility: Design your component to accept props for any nested components for maximum flexibility
  • Styling Options: Use tools like clsx to merge class overrides
  • Binding Props: Use bind: and expose $bindable props to provide consumers with full control
  • Type Safety: Use the exported types from Bits UI to type your component props

Managing Open State

Bits UI offers several approaches to manage and synchronize the Alert Dialog's open state, catering to different levels of control and integration needs.

1. Two-Way Binding

For seamless state synchronization, use Svelte's bind:open directive. This method automatically keeps your local state in sync with the dialog's internal state.

	<script lang="ts">
	import { Dialog } from "bits-ui";
	let isOpen = $state(false);
</script>
 
<button onclick={() => (isOpen = true)}>Open Dialog</button>
 
<Dialog.Root bind:open={isOpen}>
	<!-- ... -->
</Dialog.Root>

Key Benefits

  • Simplifies state management
  • Automatically updates isOpen when the dialog closes (e.g., via escape key)
  • Allows external control (e.g., opening via a separate button)

2. Change Handler

For more granular control or to perform additional logic on state changes, use the onOpenChange prop. This approach is useful when you need to execute custom logic alongside state updates.

	<script lang="ts">
	import { Dialog } from "bits-ui";
	let isOpen = $state(false);
</script>
 
<Dialog.Root
	open={isOpen}
	onOpenChange={(open) => {
		isOpen = open;
		// additional logic here.
	}}
>
	<!-- ... -->
</Dialog.Root>

Use Cases

  • Implementing custom behaviors on open/close
  • Integrating with external state management solutions
  • Triggering side effects (e.g., logging, data fetching)

3. Fully Controlled

For complete control over the dialog's open state, use the controlledOpen prop. This approach requires you to manually manage the open state, giving you full control over when and how the dialog responds to open/close events.

To implement controlled state:

  1. Set the controlledOpen prop to true on the Dialog.Root component.
  2. Provide an open prop to Dialog.Root, which should be a variable holding the current state.
  3. Implement an onOpenChange handler to update the state when the internal state changes.
	<script lang="ts">
	import { Dialog } from "bits-ui";
 
	let myOpen = $state(false);
</script>
 
<Dialog.Root controlledOpen open={myOpen} onOpenChange={(o) => (myOpen = o)}>
	<!-- ... -->
</Dialog.Root>

When to Use

  • Implementing complex open/close logic
  • Coordinating multiple UI elements
  • Debugging state-related issues

Focus Management

Proper focus management is crucial for accessibility and user experience in modal dialogs. Bits UI's Dialog component provides several features to help you manage focus effectively.

Focus Trap

By default, the Dialog implements a focus trap, adhering to the WAI-ARIA design pattern for modal dialogs. This ensures that keyboard focus remains within the Dialog while it's open, preventing users from interacting with the rest of the page.

Disabling the Focus Trap

While not recommended, you can disable the focus trap if absolutely necessary:

	<Dialog.Content trapFocus={false}>
	<!-- ... -->
</Dialog.Content>

Open Focus

When a Dialog opens, focus is automatically set to the first focusable element within Dialog.Content. This ensures keyboard users can immediately interact with the Dialog contents.

Customizing Initial Focus

To specify which element receives focus when the Dialog opens, use the onOpenAutoFocus prop on Dialog.Content:

	<script lang="ts">
	import { Dialog } from "bits-ui";
	let nameInput = $state<HTMLInputElement>();
</script>
 
<Dialog.Root>
	<Dialog.Trigger>Open Dialog</Dialog.Trigger>
	<Dialog.Content
		onOpenAutoFocus={(e) => {
			e.preventDefault();
			nameInput?.focus();
		}}
	>
		<input type="text" bind:this={nameInput} />
	</Dialog.Content>
</Dialog.Root>

Close Focus

When a Dialog closes, focus returns to the element that triggered its opening (typically the Dialog.Trigger).

Customizing Close Focus

To change which element receives focus when the Dialog closes, use the onCloseAutoFocus prop on Dialog.Content:

	<script lang="ts">
	import { Dialog } from "bits-ui";
	let nameInput = $state<HTMLInputElement>();
</script>
 
<input type="text" bind:this={nameInput} />
<Dialog.Root>
	<Dialog.Trigger>Open Dialog</Dialog.Trigger>
	<Dialog.Content
		onCloseAutoFocus={(e) => {
			e.preventDefault();
			nameInput?.focus();
		}}
	>
		<!-- ... -->
	</Dialog.Content>
</Dialog.Root>

Best Practices

  • Always maintain a clear focus management strategy for your Dialogs.
  • Ensure that focus is predictable and logical for keyboard users.
  • Test your focus management with keyboard navigation to verify its effectiveness.

Advanced Behaviors

Bits UI's Dialog component offers several advanced features to customize its behavior and enhance user experience. This section covers scroll locking, escape key handling, and interaction outside the dialog.

Scroll Lock

By default, when a Dialog opens, scrolling the body is disabled. This provides a more native-like experience, focusing user attention on the dialog content.

Customizing Scroll Behavior

To allow body scrolling while the dialog is open, use the preventScroll prop on Dialog.Content:

	<Dialog.Content preventScroll={false}>
	<!-- ... -->
</Dialog.Content>

Escape Key Handling

By default, pressing the Escape key closes an open Dialog. Bits UI provides two methods to customize this behavior.

Method 1: escapeKeydownBehavior

The escapeKeydownBehavior prop allows you to customize the behavior taken by the component when the Escape key is pressed. It accepts one of the following values:

  • 'close' (default): Closes the Dialog immediately.
  • 'ignore': Prevents the Dialog from closing.
  • 'defer-otherwise-close': If an ancestor Bits UI component also implements this prop, it will defer the closing decision to that component. Otherwise, the Dialog will close immediately.
  • 'defer-otherwise-ignore': If an ancestor Bits UI component also implements this prop, it will defer the closing decision to that component. Otherwise, the Dialog will ignore the key press and not close.

To always prevent the Dialog from closing on Escape key press, set the escapeKeydownBehavior prop to 'ignore' on Dialog.Content:

	<Dialog.Content escapeKeydownBehavior="ignore">
	<!-- ... -->
</Dialog.Content>

Method 2: onEscapeKeydown

For more granular control, override the default behavior using the onEscapeKeydown prop:

	<Dialog.Content
	onEscapeKeydown={(e) => {
		e.preventDefault();
		// do something else instead
	}}
>
	<!-- ... -->
</Dialog.Content>

This method allows you to implement custom logic when the Escape key is pressed.

Interaction Outside

By default, interacting outside the Dialog content area closes the Dialog. Bits UI offers two ways to modify this behavior.

Method 1: interactOutsideBehavior

The interactOutsideBehavior prop allows you to customize the behavior taken by the component when an interaction (touch, mouse, or pointer event) occurs outside the content. It accepts one of the following values:

  • 'close' (default): Closes the Dialog immediately.
  • 'ignore': Prevents the Dialog from closing.
  • 'defer-otherwise-close': If an ancestor Bits UI component also implements this prop, it will defer the closing decision to that component. Otherwise, the Dialog will close immediately.
  • 'defer-otherwise-ignore': If an ancestor Bits UI component also implements this prop, it will defer the closing decision to that component. Otherwise, the Dialog will ignore the event and not close.

To always prevent the Dialog from closing on Escape key press, set the escapeKeydownBehavior prop to 'ignore' on Dialog.Content:

	<Dialog.Content interactOutsideBehavior="ignore">
	<!-- ... -->
</Dialog.Content>

Method 2: onInteractOutside

For custom handling of outside interactions, you can override the default behavior using the onInteractOutside prop:

	<Dialog.Content
	onInteractOutside={(e) => {
		e.preventDefault();
		// do something else instead
	}}
>
	<!-- ... -->
</Dialog.Content>

This approach allows you to implement specific behaviors when users interact outside the Dialog content.

Best Practices

  • Scroll Lock: Consider your use case carefully before disabling scroll lock. It may be necessary for dialogs with scrollable content or for specific UX requirements.
  • Escape Keydown: Overriding the default escape key behavior should be done thoughtfully. Users often expect the escape key to close modals.
  • Outside Interactions: Ignoring outside interactions can be useful for important dialogs or multi-step processes, but be cautious not to trap users unintentionally.
  • Accessibility: Always ensure that any customizations maintain or enhance the dialog's accessibility.
  • User Expectations: Try to balance custom behaviors with common UX patterns to avoid confusing users.

By leveraging these advanced features, you can create highly customized dialog experiences while maintaining usability and accessibility standards.

Nested Dialogs

Dialogs can be nested within each other to create more complex user interfaces:

	<script lang="ts">
	import MyDialog from "$lib/components/MyDialog.svelte";
</script>
 
<MyDialog buttonText="Open first dialog">
	{#snippet title()}
		First Dialog
	{/snippet}
	{#snippet description()}
		This is the first dialog.
	{/snippet}
	<MyDialog buttonText="Open second dialog">
		{#snippet title()}
			Second Dialog
		{/snippet}
		{#snippet description()}
			This is the second dialog.
		{/snippet}
	</MyDialog>
</MyDialog>

Svelte Transitions

The Dialog component can be enhanced with Svelte's built-in transition effects or other animation libraries.

Using forceMount and child Snippets

To apply Svelte transitions to Dialog components, use the forceMount prop in combination with the child snippet. This approach gives you full control over the mounting behavior and animation of Dialog.Content and Dialog.Overlay.

	<script lang="ts">
	import { Dialog } from "bits-ui";
	import { fly, fade } from "svelte/transition";
</script>
 
<Dialog.Root>
	<!-- ... other dialog components -->
	<Dialog.Overlay forceMount>
		{#snippet child({ props, open })}
			{#if open}
				<div {...props} transition:fade>
					<!-- ... -->
				</div>
			{/if}
		{/snippet}
	</Dialog.Overlay>
	<Dialog.Content forceMount>
		{#snippet child({ props, open })}
			{#if open}
				<div {...props} transition:fly>
					<!-- ... -->
				</div>
			{/if}
		{/snippet}
	</Dialog.Content>
</Dialog.Root>

In this example:

  • The forceMount prop ensures the components are always in the DOM.
  • The child snippet provides access to the open state and component props.
  • Svelte's #if block controls when the content is visible.
  • Transition directives (transition:fade and transition:fly) apply the animations.

Best Practices

For cleaner code and better maintainability, consider creating custom reusable components that encapsulate this transition logic.

MyDialogOverlay.svelte
	<script lang="ts">
	import { Dialog, type WithoutChildrenOrChild } from "bits-ui";
	import { fade } from "svelte/transition";
	import type { Snippet } from "svelte";
 
	let {
		ref = $bindable(null),
		duration = 200,
		children,
		...restProps
	}: WithoutChildrenOrChild<Dialog.OverlayProps> & {
		duration?: number;
		children?: Snippet;
	} = $props();
</script>
 
<Dialog.Overlay forceMount bind:ref {...restProps}>
	{#snippet child({ props, open })}
		{#if open}
			<div {...props} transition:fade={{ duration }}>
				{@render children?.()}
			</div>
		{/if}
	{/snippet}
</Dialog.Overlay>

You can then use the MyDialogOverlay component alongside the other Dialog primitives throughout your application:

	<script lang="ts">
	import { Dialog } from "bits-ui";
	import { MyDialogOverlay } from "$lib/components";
</script>
 
<Dialog.Root>
	<Dialog.Trigger>Open</Dialog.Trigger>
	<Dialog.Portal>
		<MyDialogOverlay duration={300} />
		<Dialog.Content>
			<!-- ... -->
		</Dialog.Content>
	</Dialog.Portal>
</Dialog.Root>

API Reference

Dialog.Root

The root component used to set and manage the state of the dialog.

Property Type Description
open $bindable
boolean

Whether or not the dialog is open.

Default: false
onOpenChange
function

A callback function called when the open state changes.

Default: undefined
controlledOpen
boolean

Whether or not the open state is controlled or not. If true, the component will not update the open state internally, instead it will call onOpenChange when it would have otherwise, and it is up to you to update the open prop that is passed to the component.

Default: false
children
Snippet

The children content to render.

Default: undefined

Dialog.Trigger

The element which opens the dialog on press.

Property Type Description
ref $bindable
HTMLButtonElement

The underlying DOM element being rendered. You can bind to this to get a reference to the element.

Default: undefined
children
Snippet

The children content to render.

Default: undefined
child
Snippet

Use render delegation to render your own element. See delegation docs for more information.

Default: undefined
Data Attribute Value Description
data-dialog-trigger
''

Present on the trigger.

Dialog.Portal

A portal which renders the dialog into the body when it is open.

Property Type Description
to
union

Where to render the content when it is open. Defaults to the body. Can be disabled by passing null

Default: body
disabled
boolean

Whether the portal is disabled or not. When disabled, the content will be rendered in its original DOM location.

Default: false
children
Snippet

The children content to render.

Default: undefined

Dialog.Content

The content displayed within the dialog modal.

Property Type Description
onEscapeKeydown
function

Callback fired when an escape keydown event occurs in the floating content. You can call event.preventDefault() to prevent the default behavior of handling the escape keydown event.

Default: undefined
escapeKeydownBehavior
enum

The behavior to use when an escape keydown event occurs in the floating content. 'close' will close the content immediately. 'ignore' will prevent the content from closing. 'defer-otherwise-close' will defer to the parent element if it exists, otherwise it will close the content. 'defer-otherwise-ignore' will defer to the parent element if it exists, otherwise it will ignore the interaction.

Default: close
onInteractOutside
function

Callback fired when an outside interaction event completes, which is either a pointerup, mouseup, or touchend event, depending on the user's input device. You can call event.preventDefault() to prevent the default behavior of handling the outside interaction.

Default: undefined
onFocusOutside
function

Callback fired when focus leaves the dismissable layer. You can call event.preventDefault() to prevent the default behavior on focus leaving the layer.

Default: undefined
interactOutsideBehavior
enum

The behavior to use when an interaction occurs outside of the floating content. 'close' will close the content immediately. 'ignore' will prevent the content from closing. 'defer-otherwise-close' will defer to the parent element if it exists, otherwise it will close the content. 'defer-otherwise-ignore' will defer to the parent element if it exists, otherwise it will ignore the interaction.

Default: close
onOpenAutoFocus
function

Event handler called when auto-focusing the content as it is opened. Can be prevented.

Default: undefined
onCloseAutoFocus
function

Event handler called when auto-focusing the content as it is closed. Can be prevented.

Default: undefined
trapFocus
boolean

Whether or not to trap the focus within the content when open.

Default: true
forceMount
boolean

Whether or not to forcefully mount the content. This is useful if you want to use Svelte transitions or another animation library for the content.

Default: false
preventOverflowTextSelection
boolean

When true, prevents the text selection from overflowing the bounds of the element.

Default: true
preventScroll
boolean

When true, prevents the body from scrolling when the content is open. This is useful when you want to use the content as a modal.

Default: true
ref $bindable
HTMLDivElement

The underlying DOM element being rendered. You can bind to this to get a reference to the element.

Default: undefined
children
Snippet

The children content to render.

Default: undefined
child
Snippet

Use render delegation to render your own element. See delegation docs for more information.

Default: undefined
Data Attribute Value Description
data-state
enum

The state of the dialog.

data-dialog-content
''

Present on the content.

Dialog.Overlay

An overlay which covers the body when the dialog is open.

Property Type Description
forceMount
boolean

Whether or not to forcefully mount the content. This is useful if you want to use Svelte transitions or another animation library for the content.

Default: false
ref $bindable
HTMLDivElement

The underlying DOM element being rendered. You can bind to this to get a reference to the element.

Default: undefined
children
Snippet

The children content to render.

Default: undefined
child
Snippet

Use render delegation to render your own element. See delegation docs for more information.

Default: undefined
Data Attribute Value Description
data-state
enum

The state of the dialog.

data-dialog-overlay
''

Present on the overlay.

Dialog.Close

A button used to close the dialog.

Property Type Description
ref $bindable
HTMLButtonElement

The underlying DOM element being rendered. You can bind to this to get a reference to the element.

Default: undefined
children
Snippet

The children content to render.

Default: undefined
child
Snippet

Use render delegation to render your own element. See delegation docs for more information.

Default: undefined
Data Attribute Value Description
data-dialog-close
''

Present on the close button.

Dialog.Title

An accessibile title for the dialog.

Property Type Description
level
union

The heading level of the title.

Default: 3
ref $bindable
HTMLDivElement

The underlying DOM element being rendered. You can bind to this to get a reference to the element.

Default: undefined
children
Snippet

The children content to render.

Default: undefined
child
Snippet

Use render delegation to render your own element. See delegation docs for more information.

Default: undefined
Data Attribute Value Description
data-dialog-title
''

Present on the title.

Dialog.Description

An accessibile description for the dialog.

Property Type Description
ref $bindable
HTMLDivElement

The underlying DOM element being rendered. You can bind to this to get a reference to the element.

Default: undefined
children
Snippet

The children content to render.

Default: undefined
child
Snippet

Use render delegation to render your own element. See delegation docs for more information.

Default: undefined
Data Attribute Value Description
data-dialog-description
''

Present on the description.