Dialog
Basic example
Dialogs are built using the Dialog
, DialogOverlay
, DialogTitle
, and DialogDescription
components.
When the dialog’s open
prop is true
, the contents of the dialog will render. Focus will be moved inside the dialog and trapped there as the user cycles through the focusable elements. Scrolling is locked, the rest of your application UI is hidden from screen readers, and clicking outside the dialog or pressing the Escape key will fire the close
event and close the dialog.
<script>
import {
Dialog,
DialogOverlay,
DialogTitle,
DialogDescription,
} from "@rgossiaux/svelte-headlessui";
let isOpen = true;
</script>
<Dialog open={isOpen} on:close={() => (isOpen = false)}>
<DialogOverlay />
<DialogTitle>Deactivate account</DialogTitle>
<DialogDescription>
This will permanently deactivate your account
</DialogDescription>
<p>
Are you sure you want to deactivate your account? All of your data will be
permanently removed. This action cannot be undone.
</p>
<button on:click={() => (isOpen = false)}>Deactivate</button>
<button on:click={() => (isOpen = false)}>Cancel</button>
</Dialog>
Showing and hiding your dialog
Dialog
has no automatic management of its open/closed state. To show and hide your dialog, pass in a boolean value to the open
prop. When open
is true the dialog will render, and when it’s false the dialog will unmount.
The close
event is fired when an open dialog is dismissed, which happens when the user clicks outside of the contents of your dialog or presses the Escape key. You can use this callback to set open
back to false and close your dialog.
<script>
import {
Dialog,
DialogOverlay,
DialogTitle,
DialogDescription,
} from "@rgossiaux/svelte-headlessui";
let isOpen = true;
</script>
<!-- Pass `isOpen` to the `open` prop and use the `on:close` handler to set it back to false when the user clicks outside of the dialog or presses the escape key. -->
<Dialog open={isOpen} on:close={() => (isOpen = false)}>
<DialogOverlay />
<DialogTitle>Deactivate account</DialogTitle>
<DialogDescription>
This will permanently deactivate your account
</DialogDescription>
<p>
Are you sure you want to deactivate your account? All of your data will be
permanently removed. This action cannot be undone.
</p>
<!-- You can render additional buttons to dismiss your dialog by setting `isOpen` to `false`. -->
<button on:click={() => (isOpen = false)}>Deactivate</button>
<button on:click={() => (isOpen = false)}>Cancel</button>
</Dialog>
Managing focus within your dialog
For accessibility reasons, your dialog should contain at least one focusable element. By default, the Dialog
component will focus the first focusable element (by DOM order) when it is rendered, and pressing the Tab key will cycle through all additional focusable elements within the contents.
Focus is trapped within the dialog as long as it is rendered, so tabbing to the end will start cycling back through the beginning again. All other application elements outside of the dialog will be marked as inert and thus not focusable.
If you’d like something other than the first focusable element to receive initial focus when your dialog is initially rendered, you can use the initialFocus
prop:
<script>
import {
Dialog,
DialogOverlay,
DialogTitle,
DialogDescription,
} from "@rgossiaux/svelte-headlessui";
let isOpen = true;
let completeButton = null;
function completeOrder() {
// ...
}
</script>
<Dialog
open={isOpen}
on:close={() => (isOpen = false)}
initialFocus={completeButton}
>
<DialogOverlay />
<DialogTitle>Complete your order</DialogTitle>
<p>Your order is all ready!</p>
<button on:click={() => (isOpen = false)}> Cancel </button>
<button on:click={completeOrder} bind:this={completeButton}>
Complete order
</button>
</Dialog>
Styling
See here for some general notes on styling the components in this library.
Styling the overlay
Typically modal dialogs will be rendered on top of a transparent dark background. You can style the DialogOverlay
component to achieve this look.
The overlay component accepts normal styling props like style
and class
, so you can style it using any technique you like. Be sure to place it before the rest of your dialog’s contents in the DOM so that it doesn’t obscure the dialog’s interactive elements.
<script>
import {
Dialog,
DialogOverlay,
DialogTitle,
DialogDescription,
} from "@rgossiaux/svelte-headlessui";
let isOpen = true;
</script>
<Dialog open={isOpen} on:close={() => (isOpen = false)}>
<DialogOverlay
style={"position: fixed; top: 0; left: 0; background-color: rgb(0 0 0); opacity: 0.3;"}
/>
<!-- ... -->
</Dialog>
Styling the dialog
There’s nothing special about the contents of your dialog—you can use whatever HTML and CSS you please. Typical dialogs will have a max width and be centered in the screen, but fullscreen treatments on smaller screens are also common.
Using the Title and Description components
For accessibility reasons, you should use the DialogTitle
and DialogDescription
components when rendering content that labels and describes your dialog contents. They will be automatically linked to the root Dialog
component via the aria-labelledby
and aria-describedby
attributes, and their contents will be announced to users using screenreaders.
<script>
import {
Dialog,
DialogOverlay,
DialogTitle,
DialogDescription,
} from "@rgossiaux/svelte-headlessui";
let isOpen = true;
</script>
<Dialog open={isOpen} on:close={() => (isOpen = false)}>
<DialogOverlay />
<!-- Use the Title and Description components when appropriate to improve the accessibility of your custom dialogs. -->
<DialogTitle>Deactivate account</DialogTitle>
<DialogDescription>
This will permanently deactivate your account
</DialogDescription>
<!-- ... -->
</Dialog>
Rendering to a Portal
If you’ve ever implemented a dialog before in another framework, you may have come across the concept of portals. Portals let you insert components from one place in the component tree (for instance, deep within your application UI), but have them actually render to another place in the DOM entirely.
Since dialogs and their overlays take up the full page, you typically want to render them as a sibling to the root node of the page. That way, you can rely on natural DOM ordering to ensure that their content is rendered on top of your existing application UI. This also makes it easy to apply scroll locking to the rest of your application, as well as ensure that your Dialog’s contents and overlay are unobstructed to receive focus and click events.
Because of these accessibility concerns, Svelte Headless UI’s Dialog
actually uses a portal under the hood. This way we can provide features like unobstructed event handling and making the rest of your application inert. That means that when using this Dialog
, there’s no need to use a portal yourself—it’s already taken care of. You can put the <Dialog>
anywhere in your component tree and it will render to the portal.
Transitions
To animate the opening and closing of your dialog, you can use this library’s Transition component or Svelte’s built-in transition engine. See that page for a comparison.
Using the Transition
component
To use the Transition
component, all you need to do is wrap the Dialog
in a <Transition>
, and the dialog will transition automatically based on the value of the show
prop on the <Transition>
. You can remove the open
prop on the <Dialog>
, as the dialog will read the show
value from the <Transition>
automatically.
<script>
import {
Dialog,
DialogOverlay,
DialogTitle,
Transition,
} from "@rgossiaux/svelte-headlessui";
let isOpen = true;
</script>
<!-- This example uses Tailwind's transition classes -->
<Transition
show={isOpen}
enter="transition duration-100 ease-out"
enterFrom="transform scale-95 opacity-0"
enterTo="transform scale-100 opacity-100"
leave="transition duration-75 ease-out"
leaveFrom="transform scale-100 opacity-100"
leaveTo="transform scale-95 opacity-0"
>
<Dialog on:close={() => (isOpen = false)}>
<DialogOverlay />
<DialogTitle>Deactivate account</DialogTitle>
<!-- ... -->
</Dialog>
</Transition>
To animate the dialog’s overlay and contents separately, use Transition
and TransitionChild
:
<script>
import {
Dialog,
DialogOverlay,
DialogTitle,
Transition,
} from "@rgossiaux/svelte-headlessui";
let isOpen = true;
</script>
<!-- This example uses Tailwind's transition classes -->
<!-- Use the `Transition` component at the root level -->
<Transition show={isOpen}>
<Dialog on:close={() => (isOpen = false)}>
<!-- Use one `TransitionChild` to apply one transition to the overlay... -->
<TransitionChild
enter="ease-out duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="ease-in duration-200"
leaveFrom="opacity-100"
leaveTo="opacity-0"
>
<DialogOverlay />
</TransitionChild>
<!-- ...and another `TransitionChild` to apply a separate transition to the contents -->
<TransitionChild
enter="ease-out duration-300"
enterFrom="opacity-0 scale-95"
enterTo="opacity-100 scale-100"
leave="ease-in duration-200"
leaveFrom="opacity-100 scale-100"
leaveTo="opacity-0 scale-95"
>
<DialogTitle>Deactivate account</DialogTitle>
<!-- ... -->
</TransitionChild>
</Dialog>
</Transition>
Using Svelte transitions
If you wish to animate your dialogs using another technique (like Svelte’s built-in transitions), you can use the static
prop to tell the component to not manage rendering itself, so you can control it manually in another way.
<script>
import {
Dialog,
DialogOverlay,
DialogTitle,
Transition,
} from "@rgossiaux/svelte-headlessui";
import { fade } from "svelte/transition";
let isOpen = true;
</script>
{#if isOpen}
<div transition:fade>
<Dialog open={isOpen} on:close={() => (isOpen = false)} static>
<DialogOverlay />
<DialogTitle>Deactivate account</DialogTitle>
<!-- ... -->
</Dialog>
</div>
{/if}
The open
prop is still used for managing scroll locking and focus trapping, but as long as static
is present, the actual element will always be rendered regardless of the open
value, which allows you to control it yourself externally. Without static
, the exit transitions won’t work correctly.
Accessibility notes
Focus management
When the dialog’s open
prop is true
, the contents of the dialog will render and focus will be moved inside the dialog and trapped there. The first focusable element according to DOM order will receive focus, although you can use the initialFocus
prop to control which element receives initial focus. Pressing Tab on an open dialog cycles through all the focusable elements.
Mouse interaction
When a Dialog
is rendered, clicking the DialogOverlay
will close the Dialog
.
No mouse interaction to open the Dialog
is included out-of-the-box, though typically you will wire a <button />
element up with an on:click
handler that toggles the dialog’s open
prop to true
.
Keyboard interaction
Command | Description |
---|---|
<Esc> |
Closes any open dialogs |
<Tab> |
Cycles through an open dialog’s contents |
<Shift> + <Tab> |
Cycles backwards through an open dialog’s contents |
Other
When a Dialog is open, scroll is locked and the rest of your application UI is hidden from screen readers.
All relevant ARIA attributes are automatically managed.
For a full reference on all accessibility features implemented in Dialog
, see the ARIA spec on Dialogs.
Component API
Dialog
The main dialog component.
Prop | Default | Type | Description | |
---|---|---|---|---|
open |
— | boolean |
Whether the Dialog is open |
|
initialFocus |
null |
HTMLElement | null |
The element that should receive focus when the Dialog is first opened |
|
as |
div |
string |
The element the Dialog should render as |
|
static |
false |
boolean |
Whether the element should ignore the internally managed open/closed state | |
unmount |
true |
boolean |
Whether the element should be unmounted, instead of just hidden, based on the open/closed state |
Note that static
and unmount
cannot be used together.
Slot prop | Type | Description |
---|---|---|
open |
boolean |
Whether or not the dialog is open |
This component also dispatches a custom event, which is listened to using the Svelte on:
directive:
Event name | Type of event .detail |
Description |
---|---|---|
close |
null |
Emitted when the Dialog is dismissed (via the overlay or Escape key). Typically used to close the dialog by setting open to false. |
DialogOverlay
This can be used to create an overlay for your dialog. Clicking on the overlay will close the dialog.
Prop | Default | Type | Description |
---|---|---|---|
as |
div |
string |
The element the DialogOverlay should render as |
Slot prop | Type | Description |
---|---|---|
open |
boolean |
Whether or not the dialog is open |
DialogTitle
This is the title for your dialog. When this is used, it will set the aria-labelledby
on the dialog.
Prop | Default | Type | Description |
---|---|---|---|
as |
h2 |
string |
The element the DialogTitle should render as |
Slot prop | Type | Description |
---|---|---|
open |
boolean |
Whether or not the dialog is open |
DialogDescription
This is the description for your dialog. When this is used, it will set the aria-describedby
on the dialog.
Prop | Default | Type | Description |
---|---|---|---|
as |
p |
string |
The element the DialogDescription should render as |
Slot prop | Type | Description |
---|---|---|
open |
boolean |
Whether or not the dialog is open |