Dialog
A Tailwind CSS dialog component for modals, sheets, and non-modal dialogs.
<button class="btn" data-sp-toggle="dialog" data-sp-target="#demo-modal">
Open Dialog
</button>
<dialog
id="demo-modal"
class="dialog"
aria-labelledby="demo-title"
aria-describedby="demo-desc"
>
<div class="dialog-backdrop"></div>
<div class="dialog-panel">
<button
class="btn btn-ghost btn-icon absolute top-2 right-2"
aria-label="Close"
data-sp-dismiss="dialog"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>
</button>
<h2 id="demo-title" class="text-lg font-semibold mb-2">Dialog Title</h2>
<p id="demo-desc" class="text-muted-foreground mb-4">
This is a modal dialog. Click the backdrop or press Escape to close.
</p>
<div class="flex gap-2 justify-end">
<button class="btn btn-outline" data-sp-dismiss="dialog">Cancel</button>
<button class="btn" data-sp-dismiss="dialog">Confirm</button>
</div>
</div>
</dialog>Modal vs Non-modal
This component uses dialog.show() instead of dialog.showModal(), so the backdrop is what creates the modal behavior. Add .dialog-backdrop to block interaction with the page behind it, or omit it for a non-modal dialog.
<button
class="btn btn-outline"
data-sp-toggle="dialog"
data-sp-target="#modal-example"
>
Open Modal
</button>
<button
class="btn btn-outline"
data-sp-toggle="dialog"
data-sp-target="#nonmodal-example"
>
Open Non-modal
</button>
<dialog
id="modal-example"
class="dialog"
aria-labelledby="modal-title"
aria-describedby="modal-desc"
>
<div class="dialog-backdrop"></div>
<div class="dialog-panel">
<h2 id="modal-title" class="text-lg font-semibold mb-2">Modal</h2>
<p id="modal-desc" class="text-muted-foreground mb-4">
Background is blocked. Click backdrop or press Escape to close.
</p>
<button class="btn" data-sp-dismiss="dialog">Close</button>
</div>
</dialog>
<dialog
id="nonmodal-example"
class="dialog"
aria-labelledby="nonmodal-title"
aria-describedby="nonmodal-desc"
>
<div class="dialog-panel">
<h2 id="nonmodal-title" class="text-lg font-semibold mb-2">Non-modal</h2>
<p id="nonmodal-desc" class="text-muted-foreground mb-4">
You can interact with the page behind this dialog.
</p>
<button class="btn" data-sp-dismiss="dialog">Close</button>
</div>
</dialog>Static backdrop
Use data-sp-backdrop="static" to prevent closing when clicking the backdrop or pressing Escape.
<button
class="btn btn-destructive"
data-sp-toggle="dialog"
data-sp-target="#static-modal"
>
Delete
</button>
<dialog
id="static-modal"
class="dialog"
data-sp-backdrop="static"
aria-labelledby="static-title"
aria-describedby="static-desc"
>
<div class="dialog-backdrop"></div>
<div class="dialog-panel">
<h2 id="static-title" class="text-lg font-semibold mb-2">Are you sure?</h2>
<p id="static-desc" class="text-muted-foreground mb-4">
This action cannot be undone. Backdrop clicks and Escape are disabled.
</p>
<div class="flex gap-2 justify-end">
<button class="btn btn-outline" data-sp-dismiss="dialog">Cancel</button>
<button class="btn btn-destructive" data-sp-dismiss="dialog">
Delete
</button>
</div>
</div>
</dialog>Customization
Override positioning, sizing, and styles to create custom dialog variants like sheets, fullscreen modals, or custom backdrops.
<button
class="btn btn-outline"
data-sp-toggle="dialog"
data-sp-target="#sheet-example"
>
Sheet
</button>
<button
class="btn btn-outline"
data-sp-toggle="dialog"
data-sp-target="#fullscreen-example"
>
Fullscreen
</button>
<button
class="btn btn-outline"
data-sp-toggle="dialog"
data-sp-target="#gradient-example"
>
Gradient Backdrop
</button>
<dialog
id="sheet-example"
class="dialog"
aria-labelledby="sheet-title"
aria-describedby="sheet-desc"
>
<div class="dialog-backdrop"></div>
<div
class="dialog-panel no-animation top-0 left-0 h-dvh w-72 max-h-none rounded-none translate-x-0 translate-y-0 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:slide-in-from-left data-[state=closed]:slide-out-to-left duration-300"
>
<button
class="btn btn-ghost btn-icon absolute top-2 right-2"
aria-label="Close"
data-sp-dismiss="dialog"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>
</button>
<h2 id="sheet-title" class="text-lg font-semibold mb-2">Sheet</h2>
<p id="sheet-desc" class="text-muted-foreground">
This sheet slides in from the left. Great for navigation menus or
sidebars.
</p>
</div>
</dialog>
<dialog
id="fullscreen-example"
class="dialog"
aria-labelledby="fullscreen-title"
aria-describedby="fullscreen-desc"
>
<div class="dialog-backdrop"></div>
<div class="dialog-panel max-w-none h-screen">
<div class="flex justify-between items-center mb-4">
<h2 id="fullscreen-title" class="text-lg font-semibold">
Fullscreen Modal
</h2>
<button
class="btn btn-ghost btn-icon"
aria-label="Close"
data-sp-dismiss="dialog"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>
</button>
</div>
<p id="fullscreen-desc" class="text-muted-foreground">
This modal takes up the entire screen.
</p>
</div>
</dialog>
<dialog
id="gradient-example"
class="dialog"
aria-labelledby="gradient-title"
aria-describedby="gradient-desc"
>
<div
class="dialog-backdrop bg-linear-to-br from-pink-500/60 via-purple-500/60 to-cyan-500/60 backdrop-blur-sm"
></div>
<div
class="dialog-panel bg-linear-to-br from-purple-950 to-slate-900 text-white border border-purple-500/30"
>
<button
class="btn btn-ghost btn-icon absolute top-2 right-2 text-white hover:bg-white/10"
aria-label="Close"
data-sp-dismiss="dialog"
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>
</button>
<h2 id="gradient-title" class="text-lg font-semibold mb-2">
Custom Styles
</h2>
<p id="gradient-desc" class="text-purple-200 mb-4">
Override the backdrop and panel with gradients, blur, or any custom
styles.
</p>
<button
class="btn w-full bg-purple-600 hover:bg-purple-700 text-white"
data-sp-dismiss="dialog"
>
Got it
</button>
</div>
</dialog>How it works
This component uses the native HTML <dialog> element with a small JavaScript module that handles opening, closing, animations, and focus management.
Structure
A dialog consists of three parts:
<dialog class="dialog">- The native dialog element, styled to be a fixed fullscreen container.dialog-backdrop- An overlay that blocks interaction with the page (optional, omit for non-modal).dialog-panel- The content panel that holds your dialog content
<dialog id="my-dialog" class="dialog">
<div class="dialog-backdrop"></div>
<div class="dialog-panel">
<!-- Your content here -->
</div>
</dialog>Opening and closing
Use data attributes to control the dialog without writing JavaScript.
To open a dialog, add data-sp-toggle="dialog" and data-sp-target to a button:
<button data-sp-toggle="dialog" data-sp-target="#my-dialog">Open Dialog</button>To close from inside the dialog, add data-sp-dismiss="dialog" to any button. The JavaScript will find the closest parent <dialog> element and close it:
<button data-sp-dismiss="dialog">Close</button>Clicking the backdrop or pressing Escape also closes the dialog. To prevent this, add data-sp-backdrop="static" to the dialog element:
<dialog id="confirm-dialog" class="dialog" data-sp-backdrop="static">
<!-- User must click a button to close -->
</dialog>The component uses dialog.show() rather than dialog.showModal(), which allows more flexible backdrop styling. Modal behavior (blocking page interaction) comes from the .dialog-backdrop element instead.
For programmatic control, use the global StartingPointUI.dialog module:
const dialog = document.querySelector("#my-dialog");
StartingPointUI.dialog.open(dialog);
StartingPointUI.dialog.close(dialog);
StartingPointUI.dialog.toggle(dialog);Animation
The dialog includes default fade and zoom animations. When opened, the JavaScript sets data-state="open" on the backdrop and panel. When closing, it sets data-state="closed" and waits for animations to complete before calling dialog.close().
To customize animations, add no-animation to disable the defaults and use your own classes with data-[state=open]: and data-[state=closed]: selectors:
<div
class="dialog-panel no-animation
data-[state=open]:animate-in data-[state=open]:slide-in-from-bottom
data-[state=closed]:animate-out data-[state=closed]:slide-out-to-bottom"
>
<!-- Slides in from bottom instead of zooming -->
</div><button
class="btn btn-outline"
data-sp-toggle="dialog"
data-sp-target="#anim-default"
>
Default
</button>
<button
class="btn btn-outline"
data-sp-toggle="dialog"
data-sp-target="#anim-none"
>
No Animation
</button>
<button
class="btn btn-outline"
data-sp-toggle="dialog"
data-sp-target="#anim-slide"
>
Slide from Bottom
</button>
<button
class="btn btn-outline"
data-sp-toggle="dialog"
data-sp-target="#anim-slow"
>
Slow (500ms)
</button>
<dialog
id="anim-default"
class="dialog"
aria-labelledby="anim-default-title"
aria-describedby="anim-default-desc"
>
<div class="dialog-backdrop"></div>
<div class="dialog-panel">
<h2 id="anim-default-title" class="text-lg font-semibold mb-2">
Default Animation
</h2>
<p id="anim-default-desc" class="text-muted-foreground mb-4">
The backdrop fades while the panel fades and zooms.
</p>
<button class="btn" data-sp-dismiss="dialog">Close</button>
</div>
</dialog>
<dialog
id="anim-none"
class="dialog"
aria-labelledby="anim-none-title"
aria-describedby="anim-none-desc"
>
<div class="dialog-backdrop no-animation"></div>
<div class="dialog-panel no-animation">
<h2 id="anim-none-title" class="text-lg font-semibold mb-2">
No Animation
</h2>
<p id="anim-none-desc" class="text-muted-foreground mb-4">
Add the `no-animation` class to disable the default animations.
</p>
<button class="btn" data-sp-dismiss="dialog">Close</button>
</div>
</dialog>
<dialog
id="anim-slide"
class="dialog"
aria-labelledby="anim-slide-title"
aria-describedby="anim-slide-desc"
>
<div class="dialog-backdrop"></div>
<div
class="dialog-panel no-animation data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=open]:fade-in-0 data-[state=open]:slide-in-from-bottom data-[state=closed]:fade-out-0 data-[state=closed]:slide-out-to-bottom duration-300"
>
<h2 id="anim-slide-title" class="text-lg font-semibold mb-2">
Slide from Bottom
</h2>
<p id="anim-slide-desc" class="text-muted-foreground mb-4">
Use `no-animation` and add your own animation classes.
</p>
<button class="btn" data-sp-dismiss="dialog">Close</button>
</div>
</dialog>
<dialog
id="anim-slow"
class="dialog"
aria-labelledby="anim-slow-title"
aria-describedby="anim-slow-desc"
>
<div class="dialog-backdrop duration-500"></div>
<div class="dialog-panel duration-500">
<h2 id="anim-slow-title" class="text-lg font-semibold mb-2">
Slow Animation
</h2>
<p id="anim-slow-desc" class="text-muted-foreground mb-4">
Override the duration with utility classes like `duration-500`.
</p>
<button class="btn" data-sp-dismiss="dialog">Close</button>
</div>
</dialog>The default animations use tw-animate-css utilities. You can customize or replace these with your own animations.
Accessibility
This component uses the native <dialog> element for proper screen reader announcements. The dialog JavaScript module provides Escape key handling for all dialogs, and focus trapping (Tab cycles through focusable elements) for modal dialogs.
For proper screen reader support, add these attributes to your dialogs:
| Attribute | Description |
|---|---|
aria-labelledby="title-id" | Add to dialog to reference the title element |
aria-describedby="desc-id" | Add to dialog to reference the description |
aria-label="Close" | Add to icon-only buttons for screen reader labels |
<dialog aria-labelledby="title" aria-describedby="desc">
<div class="dialog-backdrop"></div>
<div class="dialog-panel">
<button aria-label="Close" data-sp-dismiss="dialog">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>
</button>
<h2 id="title">Title</h2>
<p id="desc">Description</p>
</div>
</dialog>Class reference
All available classes for the dialog component.
| Class | Description |
|---|---|
dialog | Base class for the dialog element |
dialog-backdrop | Overlay that blocks interaction with the page behind it |
dialog-panel | The content panel inside the dialog |
no-animation | Disables default animations on backdrop or panel |
<dialog class="dialog">
<div class="dialog-backdrop"></div>
<div class="dialog-panel">
<!-- Content -->
</div>
</dialog>Data attributes
All data attributes for the dialog component.
| Attribute | Description |
|---|---|
data-sp-toggle="dialog" | Add to a button to open the dialog in data-sp-target |
data-sp-target="#id" | Specifies which dialog to open |
data-sp-dismiss="dialog" | Add to a button inside the dialog to close it |
data-sp-backdrop="static" | Add to dialog to prevent closing on backdrop or Escape |
data-state | Set to open or closed on backdrop and panel |
<button data-sp-toggle="dialog" data-sp-target="#my-dialog">Open</button>
<dialog id="my-dialog" class="dialog" data-sp-backdrop="static">
<div class="dialog-backdrop"></div>
<div class="dialog-panel">
<button data-sp-dismiss="dialog">Close</button>
</div>
</dialog>