Skip to content
Destyler UI Destyler UI Destyler UI

Dialog

A dialog is a window overlaid on either the primary window or another dialog window. Content behind a modal dialog is inert, meaning that users cannot interact with it.

Features

Install

Install the component from your command line.

Terminal window
      
        
npm install @destyler/{dialog,vue}
Terminal window
      
        
npm install @destyler/{dialog,react}
Terminal window
      
        
npm install @destyler/{dialog,svelte}
Terminal window
      
        
npm install @destyler/{dialog,solid}

Anatomy

Import all parts and piece them together.

<script setup lang="ts">
import * as dialog from '@destyler/dialog'
import { normalizeProps, useMachine } from '@destyler/vue'
import { computed, useId } from 'vue'
const [state, send] = useMachine(dialog.machine({
id: useId(),
}))
const api = computed(() => dialog.connect(state.value, send, normalizeProps))
</script>
<template>
<button v-bind="api.getTriggerProps()"></button>
<div v-bind="api.getBackdropProps()" />
<div v-bind="api.getPositionerProps()">
<div v-bind="api.getContentProps()">
<h2 v-bind="api.getTitleProps()"></h2>
<p v-bind="api.getDescriptionProps()"></p>
<button v-bind="api.getCloseTriggerProps()"></button>
</div>
</div>
</template>
import * as dialog from '@destyler/dialog'
import { normalizeProps, useMachine } from '@destyler/react'
import { useId } from 'react'
export default function Dialog() {
const [state, send] = useMachine(dialog.machine({
id: useId(),
}))
const api = dialog.connect(state, send, normalizeProps)
return (
<>
<button {...api.getTriggerProps()}></button>
<div {...api.getBackdropProps()}/>
<div {...api.getPositionerProps()}>
<div {...api.getContentProps()}>
<h2 {...api.getTitleProps()}></h2>
<p {...api.getDescriptionProps()}></p>
<button {...api.getCloseTriggerProps()}></button>
</div>
</div>
</>
)
}
<script lang="ts">
import * as dialog from '@destyler/dialog'
import { normalizeProps, useMachine } from '@destyler/svelte'
const id = $props.id()
const [state, send] = useMachine(dialog.machine({ id }))
const api = $derived(dialog.connect(state, send, normalizeProps))
</script>
<button {...api.getTriggerProps()}></button>
<div {...api.getBackdropProps()}></div>
<div {...api.getPositionerProps()}>
<div {...api.getContentProps()}>
<h2 {...api.getTitleProps()}></h2>
<p {...api.getDescriptionProps()}></p>
<button {...api.getCloseTriggerProps()}></button>
</div>
</div>
import * as dialog from '@destyler/dialog'
import { normalizeProps, useMachine } from '@destyler/solid'
import { createMemo, createUniqueId } from 'solid-js'
export default function Dialog() {
const [state, send] = useMachine(dialog.machine({
id: createUniqueId(),
}))
const api = createMemo(() => dialog.connect(state, send, normalizeProps))
return (
<>
<button {...api().getTriggerProps()}></button>
<div {...api().getBackdropProps()}/>
<div {...api().getPositionerProps()}>
<div {...api().getContentProps()}>
<h2 {...api().getTitleProps()}></h2>
<p {...api().getDescriptionProps()}></p>
<button {...api().getCloseTriggerProps()}></button>
</div>
</div>
</>
)
}

Managing focus within the dialog

When the dialog opens, it automatically sets focus on the first focusable elements and traps focus within it, so that tabbing is constrained to it.

To control the element that should receive focus on open, pass the initialFocusEl context (which can be an element or a function that returns an element)

<script setup lang="ts">
import * as dialog from '@destyler/dialog'
import { normalizeProps, useMachine } from '@destyler/vue'
import { computed, useId , ref } from 'vue'
const inputRef = ref(null)
const [state, send] = useMachine(dialog.machine({
id: useId(),
initialFocusEl: () => inputRef.value,
}))
const api = computed(() => dialog.connect(state.value, send, normalizeProps))
</script>
<template>
<input ref="inputRef" />
</template>
import * as dialog from '@destyler/dialog'
import { normalizeProps, useMachine } from '@destyler/react'
import { useId, useRef } from 'react'
export default function Dialog() {
const [state, send] = useMachine(dialog.machine({
id: useId(),
initialFocusEl: () => inputRef.current,
}))
const api = dialog.connect(state, send, normalizeProps)
return (
<>
<input ref={inputRef} />
</>
)
}
<script lang="ts">
import * as dialog from '@destyler/dialog'
import { normalizeProps, useMachine } from '@destyler/svelte'
const id = $props.id()
let inputRef: HTMLInputElement | null = null
const [state, send] = useMachine(dialog.machine({
id
initialFocusEl: () => inputRef,
}))
const api = $derived(dialog.connect(state, send, normalizeProps))
</script>
<input bind:this={inputRef} />
import * as dialog from '@destyler/dialog'
import { normalizeProps, useMachine } from '@destyler/solid'
import { createMemo, createUniqueId, createSignal } from 'solid-js'
const [inputEl, setInputEl] = createSignal()
export default function Dialog() {
const [state, send] = useMachine(dialog.machine({
id: createUniqueId(),
initialFocusEl: inputEl,
}))
const api = createMemo(() => dialog.connect(state, send, normalizeProps))
return (
<>
<input ref={setInputEl} />
</>
)
}

To set the element that receives focus when the dialog closes, pass the finalFocusEl in the similar fashion as shown above.

Closing the dialog on interaction outside

By default, the dialog closes when you click its overlay. You can set closeOnInteractOutside to false if you want the modal to stay visible.

const [state, send] = useMachine(
dialog.machine({
closeOnInteractOutside: false,
}),
)

You can also customize the behavior by passing a function to the onInteractOutside context and calling event.preventDefault()

const [state, send] = useMachine(
dialog.machine({
onInteractOutside(event) {
const target = event.target
if (target?.closest("<selector>")) {
return event.preventDefault()
}
},
}),
)

Listening for open state changes

const [state, send] = useMachine(
dialog.machine({
onOpenChange(details) {
// details => { open: boolean }
console.log("open:", details.open)
},
}),
)

Controlling the scroll behavior

When the dialog is open, it prevents scrolling on the body element. To disable this behavior, set the preventScroll context to false.

const [state, send] = useMachine(
dialog.machine({
preventScroll: false,
}),
)

Creating an alert dialog

The dialog has support for dialog and alert dialog roles. It’s set to dialog by default. To change it’s role, pass the role: alertdialog property to the machine’s context.

That’s it! Now you have an alert dialog.

const [state, send] = useMachine(
dialog.machine({
role: "alertdialog",
}),
)

Styling guide

Earlier, we mentioned that each accordion part has a data-part attribute added to them to select and style them in the DOM.

[data-part="trigger"] {
/* styles for the trigger element */
}
[data-part="backdrop"] {
/* styles for the backdrop element */
}
[data-part="positioner"] {
/* styles for the positioner element */
}
[data-part="content"] {
/* styles for the content element */
}
[data-part="title"] {
/* styles for the title element */
}
[data-part="description"] {
/* styles for the description element */
}
[data-part="close-trigger"] {
/* styles for the close trigger element */
}

Open and closed state

The dialog has two states: open and closed. You can use the data-state attribute to style the dialog or trigger based on its state.

[data-part="content"][data-state="open"] {
/* styles for the open state */
}
[data-part="trigger"][data-state="open"] {
/* styles for the open state */
}

Methods and Properties

Machine Context

The dialog machine exposes the following context properties:

ids
Partial<{ trigger: string; positioner: string; backdrop: string; content: string; closeTrigger: string; title: string; description: string; }>
The ids of the elements in the dialog. Useful for composition.
trapFocus
boolean
Whether to trap focus inside the dialog when it's opened
preventScroll
boolean
Whether to prevent scrolling behind the dialog when it's opened
modal
boolean
Whether to prevent pointer interaction outside the element and hide all content below it
initialFocusEl
() => HTMLElement
Element to receive focus when the dialog is opened
finalFocusEl
() => HTMLElement
Element to receive focus when the dialog is closed
restoreFocus
boolean
Whether to restore focus to the element that had focus before the dialog was opened
onOpenChange
(details: OpenChangeDetails) => void
Callback to be invoked when the dialog is opened or closed
closeOnInteractOutside
boolean
Whether to close the dialog when the outside is clicked
closeOnEscape
boolean
Whether to close the dialog when the escape key is pressed
aria-label
string
Human readable label for the dialog, in event the dialog title is not rendered
role
"dialog" | "alertdialog"
The dialog's role
open
boolean
Whether the dialog is open
open.controlled
boolean
Whether the dialog is controlled by the user
dir
"ltr" | "rtl"
The document's text/writing direction.
id
string
The unique identifier of the machine.
getRootNode
() => Node | ShadowRoot | Document
A root node to correctly resolve document in custom environments. E.x.: Iframes, Electron.
onEscapeKeyDown
(event: KeyboardEvent) => void
Function called when the escape key is pressed
onPointerDownOutside
(event: PointerDownOutsideEvent) => void
Function called when the pointer is pressed down outside the component
onFocusOutside
(event: FocusOutsideEvent) => void
Function called when the focus is moved outside the component
onInteractOutside
(event: InteractOutsideEvent) => void
Function called when an interaction happens outside the component
persistentElements
(() => Element)[]
Returns the persistent elements that: - should not have pointer-events disabled - should not trigger the dismiss event

Machine API

The dialog api exposes the following methods:

open
boolean
Whether the dialog is open
setOpen
(open: boolean) => void
Function to open or close the dialog

Data Attributes

Trigger

name
desc
data-scope
dialog
data-part
trigger
data-state
"open" | "closed"

Backdrop

name
desc
data-scope
dialog
data-part
backdrop
data-state
"open" | "closed"

Content

name
desc
data-scope
dialog
data-part
content
data-state
"open" | "closed"

Accessibility

Keyboard Interaction

name
desc
Enter
When focus is on the trigger, opens the dialog.
Tab
Moves focus to the next focusable element within the content. Focus is trapped within the dialog.
Shift + Tab
Moves focus to the previous focusable element. Focus is trapped within the dialog.
Esc
Closes the dialog and moves focus to trigger or the defined final focus element