Dialog
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.
<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.
<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.
<button
command="show-modal"
commandfor="greeting"
>
Show Modal
</button>
<dialog id="greeting">
👋 Hi there!
</dialog>
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>
.
<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.
<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.
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.
<dialog id="light-dismiss-modal" closedby="any">
Clicking outside me will close me (but not in Safari).
</dialog>
[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.
<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.
<dialog>
<div class="action-header">
<h2>My Modal Heading</h2>
<form method="dialog">
<button
class="plain"
aria-label="Close Modal"
>
✕
</button>
</form>
</div>
The rest of the content in my modal...
</dialog>
[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.
<dialog>
<div class="action-header">
<h2>With an Action Body</h2>
<form method="dialog">
<button
class="plain"
aria-label="Close Modal"
>
✕
</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).
<dialog>
<div class="action-header">
<h2>Login</h2>
<form method="dialog">
<button class="plain" aria-label="Close Modal">
✕
</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 theEsc
key. It is cancelable withevent.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 runningevent.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 acancel
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();