Dialog
A modal window presenting content or seeking user input without navigating away from the current context.
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 sub-components 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 sub-component 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 sub-components, 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:
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.
Usage with Inline Snippets
Usage with Separate Snippets
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.
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.
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:
- Set the
controlledOpen
prop totrue
on theDialog.Root
component. - Provide an
open
prop toDialog.Root
, which should be a variable holding the current state. - Implement an
onOpenChange
handler to update the state when the internal state changes.
When to Use
- Implementing complex open/close logic
- Coordinating multiple UI elements
- Debugging state-related issues
Note
While powerful, fully controlled state should be used judiciously as it increases complexity and can cause unexpected behaviors if not handled carefully.
For more in-depth information on controlled components and advanced state management techniques, refer to our Controlled State documentation.
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:
Accessibility Warning
Disabling the focus trap may compromise accessibility. Only do this if you have a specific reason and implement an alternative focus management strategy.
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
:
Important
Always ensure that something within the Dialog receives focus when it opens. This is crucial for maintaining keyboard navigation context and makes your users happy.
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
:
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
:
Note
Enabling body scroll may affect user focus and accessibility. Use this option judiciously.
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
:
Method 2: onEscapeKeydown
For more granular control, override the default behavior using the onEscapeKeydown
prop:
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
:
Method 2: onInteractOutside
For custom handling of outside interactions, you can override the default behavior using the onInteractOutside
prop:
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:
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
.
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
andtransition:fly
) apply the animations.
Best Practices
For cleaner code and better maintainability, consider creating custom reusable components that encapsulate this transition logic.
You can then use the MyDialogOverlay
component alongside the other Dialog
primitives throughout your application:
Working with Forms
Form Submission
When using the Dialog
component, often you'll want to submit a form or perform an asynchronous action and then close the dialog.
This can be done by waiting for the asynchronous action to complete, then programmatically closing the dialog.
Inside a Form
If you're using a Dialog
within a form, you'll need to ensure that the Portal
is disabled or not included in the Dialog
structure. This is because the Portal
will render the dialog content outside of the form, which will prevent the form from being submitted correctly.
API Reference
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 Default: false |
children | Snippet | The children content to render. Default: undefined |
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 Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-dialog-trigger | '' | Present on the trigger. |
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 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 |
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 Default: undefined |
escapeKeydownBehavior | enum | The behavior to use when an escape keydown event occurs in the floating content. Default: close |
onInteractOutside | function | Callback fired when an outside interaction event completes, which is either a Default: undefined |
onFocusOutside | function | Callback fired when focus leaves the dismissible layer. You can call Default: undefined |
interactOutsideBehavior | enum | The behavior to use when an interaction occurs outside of the floating content. 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 Default: true |
preventScroll | boolean | When 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 Child Snippet 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. |
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 Child Snippet 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. |
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 Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-dialog-close | '' | Present on the close button. |
An accessible 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 Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-dialog-title | '' | Present on the title. |
An accessible 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 Child Snippet docs for more information. Default: undefined |
Data Attribute | Value | Description |
---|---|---|
data-dialog-description | '' | Present on the description. |