import { useRef } from 'react';
export default function Default() {
const dialogRef = useRef<HTMLDialogElement>(null);
function showModal() {
dialogRef.current?.showModal();
}
function closeModal() {
dialogRef.current?.close();
}
return (
<>
{/* Dialog */}
<dialog ref={dialogRef} className="dialog preset-filled-surface-100-900 animate-dialog">
<header>
<h2 className="h3">Hello world!</h2>
</header>
<article>
<p>This is an example modal created using the native Dialog element.</p>
</article>
<footer className="flex justify-end">
<form method="dialog">
<button type="button" className="btn preset-tonal" onClick={closeModal}>
Close
</button>
</form>
</footer>
</dialog>
{/* Trigger */}
<button className="btn preset-filled" onClick={showModal}>
Open Dialog
</button>
</>
);
}<script lang="ts">
let dialogRef: HTMLDialogElement;
function showModal() {
dialogRef?.showModal();
}
function closeModal() {
dialogRef?.close();
}
</script>
<!-- Dialog -->
<dialog bind:this={dialogRef} class="dialog preset-filled-surface-100-900 animate-dialog">
<header>
<h2 class="h3">Hello world!</h2>
</header>
<article>
<p>This is an example modal created using the native Dialog element.</p>
</article>
<footer class="flex justify-end">
<form method="dialog">
<button type="button" class="btn preset-tonal" onclick={closeModal}>Close</button>
</form>
</footer>
</dialog>
<!-- Trigger -->
<button class="btn preset-filled" onclick={showModal}>Open Dialog</button>Alert Dialog
Set role="alertdialog" for dialogs that interrupt the user with a message requiring a response.
import { useRef } from 'react';
export default function AlertDialog() {
const dialogRef = useRef<HTMLDialogElement>(null);
function showModal() {
dialogRef.current?.showModal();
}
function closeModal() {
dialogRef.current?.close();
}
return (
<>
{/* Dialog */}
<dialog
ref={dialogRef}
role="alertdialog"
aria-labelledby="alertdialog-title"
aria-describedby="alertdialog-description"
className="dialog animate-dialog preset-filled-error-500 [--dialog-backdrop:color-mix(in_oklab,var(--color-error-50-950)_75%,transparent)]"
>
<header>
<h2 id="alertdialog-title" className="h3">
Discard changes?
</h2>
</header>
<article>
<p id="alertdialog-description">You have unsaved changes that will be lost. This action cannot be undone.</p>
</article>
<footer className="flex justify-end gap-2">
<button type="button" className="btn preset-tonal" onClick={closeModal}>
Cancel
</button>
<button type="button" className="btn preset-filled" onClick={closeModal}>
Discard
</button>
</footer>
</dialog>
{/* Trigger */}
<button className="btn preset-filled" onClick={showModal}>
Open Dialog
</button>
</>
);
}<script lang="ts">
let dialogRef: HTMLDialogElement;
function showModal() {
dialogRef?.showModal();
}
function closeModal() {
dialogRef?.close();
}
</script>
<!-- Dialog -->
<dialog
bind:this={dialogRef}
role="alertdialog"
aria-labelledby="alertdialog-title"
aria-describedby="alertdialog-description"
class="dialog animate-dialog preset-filled-error-500 [--dialog-backdrop:color-mix(in_oklab,var(--color-error-50-950)_75%,transparent)]"
>
<header>
<h2 id="alertdialog-title" class="h3">Discard changes?</h2>
</header>
<article>
<p id="alertdialog-description">You have unsaved changes that will be lost. This action cannot be undone.</p>
</article>
<footer class="flex justify-end gap-2">
<button type="button" class="btn preset-tonal" onclick={closeModal}>Cancel</button>
<button type="button" class="btn preset-filled" onclick={closeModal}>Discard</button>
</footer>
</dialog>
<!-- Trigger -->
<button class="btn preset-filled" onclick={showModal}>Open Dialog</button>Non-Modal
Call show() instead of showModal() to open a dialog without a backdrop or top-layer placement. Similar to a tooltip.
import { useRef } from 'react';
export default function NonModal() {
const dialogRef = useRef<HTMLDialogElement>(null);
function toggle() {
const dialog = dialogRef.current;
if (!dialog) return;
if (dialog.open) {
dialog.close();
} else {
dialog.show();
}
}
return (
<div className="relative">
{/* Dialog */}
<dialog
ref={dialogRef}
closedby="any"
className="dialog preset-filled animate-dialog [--dialog-top:auto] [--dialog-left:50%] [--dialog-translate:-50%_0] bottom-full mb-2"
>
<p>This acts as a tooltip.</p>
</dialog>
{/* Trigger */}
<button className="btn preset-filled" onClick={toggle}>
Toggle Dialog
</button>
</div>
);
}<script lang="ts">
let dialogRef: HTMLDialogElement;
function toggle() {
if (!dialogRef) return;
if (dialogRef.open) {
dialogRef.close();
} else {
dialogRef.show();
}
}
</script>
<div class="relative">
<!-- Dialog -->
<dialog
bind:this={dialogRef}
closedby="any"
class="dialog preset-filled-surface-100-900 animate-dialog [--dialog-top:auto] [--dialog-left:50%] [--dialog-translate:-50%_0] bottom-full mb-2 whitespace-nowrap"
>
<p>This acts as a tooltip.</p>
</dialog>
<!-- Trigger -->
<button class="btn preset-filled" onclick={toggle}>Toggle Dialog</button>
</div>Fullscreen
Add dialog-fullscreen to expand the dialog to the full viewport.
import { useEffect, useRef } from 'react';
export default function Fullscreen() {
const dialogRef = useRef<HTMLDialogElement>(null);
useEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return;
function onClose() {
document.body.style.overflow = '';
}
dialog.addEventListener('close', onClose);
return () => dialog.removeEventListener('close', onClose);
}, []);
function showModal() {
document.body.style.overflow = 'hidden';
dialogRef.current?.showModal();
}
function closeModal() {
dialogRef.current?.close();
}
return (
<>
{/* Dialog */}
<dialog ref={dialogRef} className="dialog dialog-fullscreen preset-filled-surface-100-900 animate-dialog">
<header>
<h2 className="h3">Hello world!</h2>
</header>
<article>
<p>This dialog expands to fill the entire viewport and locks page scrolling while open.</p>
</article>
<footer className="flex justify-end">
<form method="dialog">
<button type="button" className="btn preset-tonal" onClick={closeModal}>
Close
</button>
</form>
</footer>
</dialog>
{/* Trigger */}
<button className="btn preset-filled" onClick={showModal}>
Open Dialog
</button>
</>
);
}<script lang="ts">
let dialogRef: HTMLDialogElement;
function showModal() {
document.body.style.overflow = 'hidden';
dialogRef?.showModal();
}
function closeModal() {
dialogRef?.close();
}
function onClose() {
document.body.style.overflow = '';
}
</script>
<!-- Dialog -->
<dialog bind:this={dialogRef} onclose={onClose} class="dialog dialog-fullscreen preset-filled-surface-100-900 animate-dialog">
<header>
<h2 class="h3">Hello world!</h2>
</header>
<article>
<p>This dialog expands to fill the entire viewport and locks page scrolling while open.</p>
</article>
<footer class="flex justify-end">
<form method="dialog">
<button type="button" class="btn preset-tonal" onclick={closeModal}>Close</button>
</form>
</footer>
</dialog>
<!-- Trigger -->
<button class="btn preset-filled" onclick={showModal}>Open Dialog</button>Animation
Add animate-dialog to opt into a fade transition for the dialog and its backdrop.
import { useRef } from 'react';
export default function Animation() {
const dialogRef = useRef<HTMLDialogElement>(null);
function showModal() {
dialogRef.current?.showModal();
}
function closeModal() {
dialogRef.current?.close();
}
return (
<>
{/* Dialog */}
<dialog ref={dialogRef} className="dialog animate-dialog preset-filled-surface-100-900">
<header>
<h2 className="h3">Hello world!</h2>
</header>
<article>
<p>Opening and closing this dialog fades the surface and backdrop.</p>
</article>
<footer className="flex justify-end">
<form method="dialog">
<button type="button" className="btn preset-tonal" onClick={closeModal}>
Close
</button>
</form>
</footer>
</dialog>
{/* Trigger */}
<button className="btn preset-filled" onClick={showModal}>
Open Dialog
</button>
</>
);
}<script lang="ts">
let dialogRef: HTMLDialogElement;
function showModal() {
dialogRef?.showModal();
}
function closeModal() {
dialogRef?.close();
}
</script>
<!-- Dialog -->
<dialog bind:this={dialogRef} class="dialog animate-dialog preset-filled-surface-100-900">
<header>
<h2 class="h3">Hello world!</h2>
</header>
<article>
<p>Opening and closing this dialog fades the surface and backdrop.</p>
</article>
<footer class="flex justify-end">
<form method="dialog">
<button type="button" class="btn preset-tonal" onclick={closeModal}>Close</button>
</form>
</footer>
</dialog>
<!-- Trigger -->
<button class="btn preset-filled" onclick={showModal}>Open Dialog</button>Interaction
Light Dismiss
Set closedby="any" to allow the user to close the dialog by clicking the backdrop or pressing Esc.
import { useRef } from 'react';
export default function LightDismiss() {
const dialogRef = useRef<HTMLDialogElement>(null);
function showModal() {
dialogRef.current?.showModal();
}
function closeModal() {
dialogRef.current?.close();
}
return (
<>
{/* Dialog */}
<dialog ref={dialogRef} closedby="any" className="dialog preset-filled-surface-100-900 animate-dialog">
<header>
<h2 className="h3">Click outside to close</h2>
</header>
<article>
<p>This dialog supports light dismiss. Click the backdrop or press Esc to close.</p>
</article>
<footer className="flex justify-end">
<form method="dialog">
<button type="button" className="btn preset-tonal" onClick={closeModal}>
Close
</button>
</form>
</footer>
</dialog>
{/* Trigger */}
<button className="btn preset-filled" onClick={showModal}>
Open Dialog
</button>
</>
);
}<script lang="ts">
let dialogRef: HTMLDialogElement;
function showModal() {
dialogRef?.showModal();
}
function closeModal() {
dialogRef?.close();
}
</script>
<!-- Dialog -->
<dialog bind:this={dialogRef} closedby="any" class="dialog preset-filled-surface-100-900 animate-dialog">
<header>
<h2 class="h3">Click outside to close</h2>
</header>
<article>
<p>This dialog supports light dismiss. Click the backdrop or press Esc to close.</p>
</article>
<footer class="flex justify-end">
<form method="dialog">
<button type="button" class="btn preset-tonal" onclick={closeModal}>Close</button>
</form>
</footer>
</dialog>
<!-- Trigger -->
<button class="btn preset-filled" onclick={showModal}>Open Dialog</button>⚠️ View Browser Support
| Browser | Minimum Version | Released |
|---|---|---|
| Safari | 18.2 | December 2024 |
| Safari on iOS | 18.2 | December 2024 |
| Chrome | 134 | March 2025 |
| Edge | 134 | March 2025 |
| Chrome for Android | 134 | March 2025 |
| Firefox | 136 | March 2025 |
| Firefox for Android | 136 | March 2025 |
Result Handling
Wrap controls in a <form method="dialog"> , then read each submit button’s value from dialog.returnValue on close .
Last result: —
import { useEffect, useRef, useState } from 'react';
export default function Result() {
const dialogRef = useRef<HTMLDialogElement>(null);
const [result, setResult] = useState<string>('—');
useEffect(() => {
const dialog = dialogRef.current;
if (!dialog) return;
function onClose() {
setResult(dialog!.returnValue || '(dismissed)');
}
dialog.addEventListener('close', onClose);
return () => dialog.removeEventListener('close', onClose);
}, []);
function showModal() {
dialogRef.current?.showModal();
}
return (
<>
{/* Dialog */}
<dialog ref={dialogRef} className="dialog preset-filled-surface-100-900 animate-dialog">
<header>
<h2 className="h3">Confirm action</h2>
</header>
<article>
<p>Submitting either button closes the dialog and exposes its value via the dialog's returnValue.</p>
</article>
<footer className="flex justify-end gap-2">
<form method="dialog" className="flex gap-2">
<button type="submit" value="cancel" className="btn preset-tonal">
Cancel
</button>
<button type="submit" value="confirm" className="btn preset-filled">
Confirm
</button>
</form>
</footer>
</dialog>
{/* Trigger */}
<div className="flex flex-col items-center gap-3">
<button className="btn preset-filled" onClick={showModal}>
Open Dialog
</button>
<p className="text-sm opacity-75">
Last result: <code>{result}</code>
</p>
</div>
</>
);
}Last result: —
<script lang="ts">
let dialogRef: HTMLDialogElement;
let result = $state('—');
function showModal() {
dialogRef?.showModal();
}
function onClose() {
result = dialogRef.returnValue || '(dismissed)';
}
</script>
<!-- Dialog -->
<dialog bind:this={dialogRef} onclose={onClose} class="dialog preset-filled-surface-100-900 animate-dialog">
<header>
<h2 class="h3">Confirm action</h2>
</header>
<article>
<p>Submitting either button closes the dialog and exposes its value via the dialog's returnValue.</p>
</article>
<footer class="flex justify-end gap-2">
<form method="dialog" class="flex gap-2">
<button type="submit" value="cancel" class="btn preset-tonal">Cancel</button>
<button type="submit" value="confirm" class="btn preset-filled">Confirm</button>
</form>
</footer>
</dialog>
<!-- Trigger -->
<div class="flex flex-col items-center gap-3">
<button class="btn preset-filled" onclick={showModal}>Open Dialog</button>
<p class="text-sm opacity-75">Last result: <code>{result}</code></p>
</div>Invoker Commands
Use the Invoker Commands API to control state with pure HTML.
<!-- Dialog -->
<dialog id="invoker-dialog" class="dialog preset-filled-surface-100-900 animate-dialog">
<header>
<h2 class="h3">Hello world!</h2>
</header>
<article>
<p>This modal is controlled using HTML invoker commands — no JavaScript required.</p>
</article>
<footer class="flex justify-end">
<button type="button" class="btn preset-tonal" command="close" commandfor="invoker-dialog">Close</button>
</footer>
</dialog>
<!-- Trigger -->
<button type="button" class="btn preset-filled" command="show-modal" commandfor="invoker-dialog">Open Dialog</button>⚠️ View Browser Support
| Browser | Minimum Version | Released |
|---|---|---|
| Chrome | 135 | April 2025 |
| Edge | 135 | April 2025 |
| Chrome for Android | 135 | April 2025 |
| Firefox | 144 | October 2025 |
| Firefox for Android | 144 | October 2025 |
| Safari | 26.2 | December 2025 |
| Safari on iOS | 26.2 | December 2025 |
Alternatives
Explore Skeleton’s framework components for more advanced features and control: