# Dialogs

Display modal popups using the native HTML dialog element.

```tsx
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>
		</>
	);
}

```

## Alert Dialog

Set [`role="alertdialog"`](https://developer.mozilla.org/en-US/docs/Web/Accessibility/ARIA/Reference/Roles/alertdialog_role) for dialogs that interrupt the user with a message requiring a response.

```tsx
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>
		</>
	);
}

```

## Non-Modal

Call [`show()`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/show) instead of `showModal()` to open a dialog without a backdrop or top-layer placement. Similar to a tooltip.

```tsx
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>
	);
}

```

## Fullscreen

Add `dialog-fullscreen` to expand the dialog to the full viewport.

```tsx
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>
		</>
	);
}

```

## Animation

Add `animate-dialog` to opt into a fade transition for the dialog and its backdrop.

```tsx
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>
		</>
	);
}

```

## Interaction

### Light Dismiss

Set [`closedby="any"`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog#closedby) to allow the user to close the dialog by clicking the backdrop or pressing Esc.

```tsx
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>
		</>
	);
}

```

<details class="disclosure">
  <summary>⚠️ View Browser Support</summary>

  <div class="disclosure-content">
    \| 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    |
  </div>
</details>

### Result Handling

Wrap controls in a [`<form method="dialog">`](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog#usage_notes), then read each submit button's `value` from `dialog.returnValue` on [`close`](https://developer.mozilla.org/en-US/docs/Web/API/HTMLDialogElement/close_event).

```tsx
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>
		</>
	);
}

```

### Invoker Commands

Use the [Invoker Commands API](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog#modal_dialogs_using_invoker_commands) to control state with pure HTML.

```astro
<!-- 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 &mdash; 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>

```

<details class="disclosure">
  <summary>⚠️ View Browser Support</summary>

  <div class="disclosure-content">
    \| 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 |
  </div>
</details>

***

## Alternatives

Explore Skeleton's framework components for more advanced features and control:

* [Dialog](/docs/\[framework]/framework-components/dialog)
* [Popover](/docs/\[framework]/framework-components/popover)
* [Portal](/docs/\[framework]/framework-components/portal)
