- Published on
Show Dialog With HTML Dialog Tag And Alpine.js
- Authors

- Name
- Kiet
Displaying a modal is a common task on websites today. However, in the world of web development, I was quite surprised to find that even something as simple as implementing a dialog can be cumbersome, especially for a mobile developer like me. We often need to implement the logic for showing and hiding modals from scratch, locking scroll, handling dismissible behavior, and more. That's why I wanted to create a reusable component or code snippet that can be used in any future projects.
Build directive
In this article, I'll demonstrate how to implement modals using Alpine.js with a custom directive. The following code will use TypeScript.
First, define the DirectiveCallback
const dialogDirective: DirectiveCallback = function (
el: ElementWithXAttributes,
{expression, modifiers},
{cleanup, evaluateLater, effect}
) {
// Implementation goes here
}
Let's start with the el parameter:
const element = el as HTMLDialogElement;
The el parameter provides us with the element that uses our directive. Since we intend to use the directive in HTML dialog tags, we need to cast the element to HTMLDialogElement. Normal HTMLElements won't have the showModal() or close() methods.
We also provide the directive with some features:
const dismissible = modifiers.includes("dismissible");
const escapable = modifiers.includes("esc");
const lockScroll = modifiers.includes("noscroll");
I designed this directive to make the dialog dismissible, allowing users to click outside the dialog to close it. Another feature is the ability to close the dialog using the esc key on the keyboard. If you want to disable window scrolling when the modal is open, I also provide noscroll modifier.
Save the callback when the value passed to the element changed for using later.
let onChanged = evaluateLater(expression);
then use effect to track the new value to show or hide the modal:
let onChanged = evaluateLater(expression);
effect(() => { // [tl! **:8 ~~:8]
onChanged(newValue => {
if (newValue === true) {
element.showModal();
} else {
element.close();
}
});
});
I also save a callback to set the state of the dialog to 'false' (close the modal):
let overrideShowModalValue = evaluateLater(`${expression} = false`);
Add lock scroll logic when show modal
effect(() => {
onChanged(newValue => {
if (newValue === true) {
element.showModal();
if (lockScroll) { // [tl! **:2 ~~:2]
document.body.style.overflow = "hidden";
}
} else {
element.close();
if (lockScroll) { // [tl! **:2 ~~:2]
document.body.style.overflow = "";
}
}
});
});
Add escape feature
Define the callback use to close the modal when the user presses the esc key:
function keydownHandler(event: {
key: string;
preventDefault: () => void;
}) {
if (event.key !== "Escape") return;
// By default, `esc` will close the modal
event.preventDefault();
if (escapable) {
overrideShowModalValue();
}
}
Next, add logic to register and remove the listener:
effect(() => {
onChanged(newValue => {
if (newValue === true) {
element.showModal();
if (lockScroll) {
document.body.style.overflow = "hidden";
}
document.addEventListener("keydown", keydownHandler); // [tl! ** ~~]
} else {
element.close();
if (lockScroll) {
document.body.style.overflow = "";
}
document.removeEventListener("keydown", keydownHandler); // [tl! ** ~~]
}
});
});
Add dismissible feature
This function allows us to close the modal when the user clicks outside it:
function clickEventHandler(
event: {
clientY: number;
clientX: number;
}
) {
const dialogRect = el.getBoundingClientRect();
const isInDialog =
dialogRect.top <= event.clientY &&
event.clientY <= dialogRect.top + dialogRect.height &&
dialogRect.left <= event.clientX &&
event.clientX <= dialogRect.left + dialogRect.width;
if (isInDialog) return;
overrideShowModalValue();
}
add logic to register and remove the listener:
effect(() => {
onChanged(newValue => {
if (newValue === true) {
element.showModal();
if (lockScroll) {
document.body.style.overflow = "hidden";
}
document.addEventListener("keydown", keydownHandler);
if (dismissible) { // [tl! **:2 ~~:2]
el.addEventListener("click", clickEventHandler);
}
} else {
element.close();
if (lockScroll) {
document.body.style.overflow = "";
}
document.removeEventListener("keydown", keydownHandler);
el.removeEventListener("click", clickEventHandler);// [tl! ** ~~]
}
});
});
Finally, we clean up when the element is removed from the DOM:
cleanup(() => {
document.removeEventListener("keydown", keydownHandler);
el.removeEventListener("click", clickEventHandler);
});
Usage
<div x-data="{open: false}">
<button @click="open = true">Show modal</button>
<dialog x-dialog.esc.dismissible.noscroll="open">
This is modal. Click outside or use ESC key to dismiss.
</dialog>
</div>