--- title: Lustre / Gleam: How to create modal popup date: 2025-10-14 16:09:32.171849 UTC --- Modal dialog is a often used UI element, but in web tech, developers used to spend quite much effort to implement it, before the introduction of CSS frameworks like [Bootstrap](https://getbootstrap.com/2.3.2/javascript.html#modals). It was tricky to position the dialog in the center of the window, to blur the content behind it. It was tricky to let user close the popup by clicking outside it. It is until the HTML standard introduces the native [``](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/dialog) element. Now developers can create a modal popup with ease, except if their users are using Safari, which often lags behind the modern web features. Put Safari aside, now we see how to create the native modal popup in [Lustre](https://hexdocs.pm/lustre/) framework ([Gleam](https://gleam.run/) language). My use case is the [post compose form](https://github.com/hongquan/QuanWeb) of this blog. The original content is in Markdown. The form has a "Preview" button so that if I click it, a modal popup appears and loads the rendered HTML of that Markdown code. ![Form](https://cdn.imgchest.com/files/b674dc97cbd0.png) ![Preview popup](https://cdn.imgchest.com/files/07aa43f8d861.png) The native modal popup has its own HTML code, `` with an `open` attribute, you may think that we can just create it with Lustre's `html` functions, like: ```gleam html.dialog([attribute.attribute("open", "open")], []) ``` It works, but not useful, because the dialog then is non-modal: It doesn't auto-close by clicking outside or by pressing `Esc` key, it doesn't block the interaction of the UI behind. To make it modal, we won't set the "open" attribute manually, but call the `HTMLDialogElement.showModal()` method. First, let define a view function to prepare the markup for this dialog and the button to activate it: ```gleam pub const selector_post_body_preview_dialog = "post-body-preview" pub fn render_post_edit_page(id: String, model: Model) { let preview_dialog = h.dialog( [ a.class(selector_post_body_preview_dialog), a.class( "p-4 w-screen sm:start-1/2 sm:-translate-x-1/2 sm:top-1/2 sm:-translate-y-1/2 sm:w-120 md:w-220 h-120 rounded-lg", ), a.attribute("closedby", "any"), ], [ h.h1([a.class("text-xl text-center")], [h.text("Preview")]), h.hr([a.class("pb-4")]), h.iframe([ a.class("w-full h-full"), ]), ], ) let preview_button = h.button( [ a.type_("button"), a.class( "px-4 py-1.5 text-sm font-medium rounded-md text-gray-600 transition-colors duration-200 sm:text-base dark:hover:bg-gray-800 dark:text-gray-300 hover:bg-gray-100 border border-gray-400 dark:border-gray-700 cursor-pointer", ), ], [h.text("Preview")], ) } ``` Lustre doesn't have any utility for referencing a rendered DOM element, something like Vue's [template ref](https://vuejs.org/guide/essentials/template-refs), so we we need to set a static class name for the dialog, in order to find it and call `showModal()` on it later. My real use case has more steps: After user clicks "Preview" button, we grab the content from the editor (`