Skip to main content Accessibility Feedback
Skip to Documentation Content

Dialog

New! This component was just added to Kelp. Enjoy!

A dialog is used to display content that temporarily blocks interactions with and floats on top of the main content.

Kelp’s dialogs come in two flavors: modal and off-canvas drawer.

Modals

Modals float in the center of the viewport, and automatically grow in height to accommodate their content.

To create a modal, wrap your content in a <dialog> element.

👋 Hi there! This is a modal…

<dialog>
	This is a modal...
</dialog>

Drawers

Drawers are aligned to the edge of the screen, and are typically used for things like navigation menus, shopping carts, and more.

Drawers can be aligned to the left or right (start or end, respectively). The default is the end.

Wrap your content in a <dialog> element, and add the .drawer class. If you want your drawer aligned to the start of the viewport, use .drawer-start class instead.

👋 Hi there! This is a drawer aligned to the start of the viewport…

👋 Hi there! This is a drawer aligned to the end of the viewport…

<dialog class="drawer">
	This is a drawer aligned to the end of the viewport...
</dialog>

<dialog class="drawer-start">
	This is a drawer aligned to the start of the viewport...
</dialog>

Opening

Dialogs can be opened declaratively with just HTML attributes, or with JavaScript.

Opening with JavaScript

Use the .showModal() method on your <dialog> element to open it.

const modal = document.querySelector('#my-modal');
modal.showModal();

Opening with Attributes

The browser-native Invoker Command API lets you run JavaScript commands with HTML attributes.

Add the [command="show-modal"] attribute to a <button>. This tells the browser to run the .showModal() method when it’s clicked.

You also need to include the [commandfor] attribute, with the ID of your <dialog> element as its value. This tells the browser which element to run the [command] on.

👋 Hi there!

<button 
	command="show-modal"
	commandfor="greeting"
>
	Show Modal
</button>

<dialog id="greeting">
	👋 Hi there!
</dialog>
Heads up! The Invoker Command API does not have universal browser support yet, but Kelp includes a polyfill from API author Keith Cirkel under the MIT license. It will be removed in a future release once browser support improves.

Alternatively, you can open a modal directly using the .showModal() method.

const modal = document.querySelector('#my-modal');
modal.showModal();

Dismissing

Dialogs can be dismissed declaratively with just HTML attributes, or with JavaScript.

In most cases, the browser automatically shifts focus back to the <button> that triggered the <dialog> to open.

Dismissing with JavaScript

Use the .close() method on your <dialog> element to dismiss it.

const modal = document.querySelector('#my-modal');
modal.close();

Dismissing with the [command] attribute

You can use the [command] attribute to run the .close() method on your <dialog>.

❤️ See you real soon!

<dialog id="goodbye">
	<p>See you real soon!</p>

	<button 
		command="close"
		commandfor="goodbye"
	>
		Close
	</button>
</dialog>

Dismissing with the [formmethod="dialog"] attribute

Adding the [method="dialog"] attribute to any <form> inside a <dialog> will prevent it from submitting, and instead close the <dialog>.

Unlike the [command] attribute, you do not need to provide an ID of the <dialog> to target. It will always close the parent <dialog> element.

<dialog id="my-modal">
	<form method="dialog">
		<button>
			Close
		</button>
	</form>
</dialog>

If you have a <form> that should be submitted, you can add the [formmethod="dialog"] to a <button> and it will close the <dialog> element without submitting the form.

This is useful when you have a cancel button inside an otherwise functional form.

This won’t really submit anywhere…

<dialog>
	<form action="/login">
		<label for="username">Username</label>
		<input type="text" id="username" name="username">

		<button>Submit</button>
		<button formmethod="dialog">Cancel</button>
	</form>
</dialog>

Closed By

By default, Clicking outside of a <dialog> modal will not close it.

To dismiss this modal, click the Close button or press the Esc key.

The [closedby] attribute controls how a <dialog> can be dismissed.

Adding the [closedby="any"] attribute to the <dialog> element enables light dismissal, and will dismiss the modal if you click or tap on the backdrop.

Clicking outside me will close me (but not in Safari).

<dialog id="light-dismiss-modal" closedby="any">
	Clicking outside me will close me (but not in Safari).
</dialog>
Heads up! The [closedby] attribute is not yet supported in Safari. If this is critical functionality, you might consider using a polyfill.

Prevent Closing

Using a value of none for the [closedby] attribute will prevent the <dialog> from being closed by anything other than the JavaScript .close() method or an explicit close button.

Both light dismiss and the Esc key will be disabled.

This is useful when you have a <dialog> modal that requires explicit user consent or interaction before they can continue.

This can only be closed by clicking the button below (except in Safari).

<dialog id="no-dismiss-modal" closedby="none">
	This can only be closed by clicking the button below (except in Safari).
	<form method="dialog">
		<button class="secondary">
			Close
		</button>
	</form>
</dialog>

Header Actions

It’s common for <dialog> modals to have a heading and close button with an icon in them.

The .action-header layout provides an easy way to do that. It displays your heading and close button side-by-side, and adjusts the padding, margin, and alignment on the close button.

Pair it with the .plain class on your <button> and your icon of choice.

My Modal Heading

The rest of the content in my modal…

<dialog>
	<div class="action-header">
		<h2>My Modal Heading</h2>
		<form method="dialog">
			<button 
				class="plain" 
				aria-label="Close Modal"
			>
				&#x2715;
			</button>
		</form>
	</div>
	
	The rest of the content in my modal...
</dialog>
Accessibility Tip. If you’re using an icon without text for your close button, don’t forget to add an [aria-label] attribute or some .visually-hidden text so that people who use screen readers know what the button does.

Autoscrolling

A <dialog> element can never exceed to the height of the viewport. If the content is too long, Kelp’s <dialog> modals automatically add scrolling.

This doesn’t always look the best, though.

If you wrap your content in an .action-body element, Kelp will automatically turn your .action-header into a sticky header, and scroll just the .action-body content. You can optionally add an .action-footer element for sticky footer items.

Automatic Scrolling

Here’s some body text inside the modal.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

With an Action Body

Here’s some body text inside the modal.

.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.

<dialog>

	<div class="action-header">
		<h2>With an Action Body</h2>
		<form method="dialog">
			<button 
				class="plain" 
				aria-label="Close Modal"
			>
				&#x2715;
			</button>
		</form>
	</div>

	<div class="action-body">
		Really long body content...
	</div>

	<form 
		class="action-footer" 
		method="dialog"
	>
		<button>Close</button>
	</form>

</dialog>

Autofocus

By default, the first interactive element inside a <dialog> will receive focus when it’s opened. Often, that’s the close/dismiss button in the .action-header.

You can use the [autofocus] attribute to focus on a different element by default when the <dialog> first opens.

The element must be focusable (input, button, a, and so on).

Login

<dialog>
	<div class="action-header">
		<h2>Login</h2>
		<form method="dialog">
			<button class="plain" aria-label="Close Modal">
				&#x2715;
			</button>
		</form>
	</div>

	<form action="/login">
		<label for="username">Username</label>
		<input 
			type="text" 
			id="username" 
			name="username" 
			autofocus
		>

		<button>Submit</button>
		<button formmethod="dialog">Cancel</button>
	</form>
</dialog>

Styling

Kelp provides a handful of CSS variables on the <dialog> element for easier styling and customization.

@layer kelp.extend {

	dialog {
		--backdrop-opacity: 0.5;
		--background-color: var(--color-background);
		--border-color: var(--color-border-muted);
		--color: var(--color-text-normal);
		--gap: var(--size-2xl);
		--width: 32em;
	}

}

These can be added to a custom CSS file to override global styles, or used directly inline on a <dialog> element for one-off customizations.

For example, to make a single modal a bit wider than normal, you could do this.

<dialog style="--width: 40em;">
	<!-- ... -->
</dialog>

Events

The browser emits several events on the <dialog> element.

  • cancel emits when the user closes a <dialog> with light dismiss or the Esc key. It is cancelable with event.preventDefault(), but does not bubble.
  • close emits when a modal has been closed. It is not cancelable and does not bubble.
  • command emits on a <dialog> when a <button> that controls it with the [command] attribute is clicked, tapped, or activated.
    • cancelable - you can stop the <dialog> from opening or closing by running event.preventDefault().
    • event.action - the value of the [command] property.

Because these events do not bubble, we must attach our listeners to a specific <dialog> element or use the capture option.

document.addEventListener('cancel', (event) => {
	console.log('cancelled', event.target);
}, {capture: true});

document.addEventListener('close', (event) => {
	console.log('A dialog was closed', event.target);
}, {capture: true});

document.addEventListener('command', (event) => {
	console.log('A dialog was controlled by a [command] button', event.target);
	console.log('The command is:', event.action);
	
	// Stop the command from running
	event.preventDefault();
}, {capture: true});

Methods

The <dialog> element has a few methods you can manually run if needed.

  • .showModal() - Open the <dialog> element.
  • .close() - Close the <dialog> element.
  • .requestClose() - Close the <dialog> but emit a cancel event first (not universally supported yet).
const modal = document.querySelector('#my-modal');

// Open the modal/drawer
modal.showModal();

// Close the modal
modal.close();

// Close the modal with a cancel event
modal.requestClose();