Modal dialog with Hyperscript and PicoCSS
Monday 10 November 2025 Β· 18 mins read Β· Viewed 3 timesTable of contents π
TL;DR π
1<button _="on click call #register-dialog.showModal()">Open dialog</button>
2<dialog id="register-dialog">
3 <article
4 _="on click[#register-dialog.open and event.target.matches('dialog')] from elsewhere call #register-dialog.close()"
5 >
6 <header>
7 <button
8 aria-label="Close"
9 rel="prev"
10 _="on click call #register-dialog.close()"
11 ></button>
12 <p>
13 <strong>ποΈ Thank You for Registering!</strong>
14 </p>
15 </header>
16 <p>
17 We're excited to have you join us for our upcoming event. Please arrive at
18 the museum on time to check in and get started.
19 </p>
20 <ul>
21 <li>Date: Saturday, April 15</li>
22 <li>Time: 10:00am - 12:00pm</li>
23 </ul>
24 </article>
25</dialog>
How-to create a dialog modal with PicoCSS and vanilla JS π
PicoCSS offers a dialog modal directly in the CSS, without any additional JS:
1<dialog open>
2 <article>
3 <header>
4 <button aria-label="Close" rel="prev"></button>
5 <p>
6 <strong>ποΈ Thank You for Registering!</strong>
7 </p>
8 </header>
9 <p>
10 We're excited to have you join us for our upcoming event. Please arrive at
11 the museum on time to check in and get started.
12 </p>
13 <ul>
14 <li>Date: Saturday, April 15</li>
15 <li>Time: 10:00am - 12:00pm</li>
16 </ul>
17 </article>
18</dialog>
Credits to PicoCSS docs
However, there is a small issue: the example is not interactive!
Thankfully, PicoCSS also offers an example to open and close using JS. However, the give is quite long:
1/*
2 * Modal
3 *
4 * Pico.css - https://picocss.com
5 * Copyright 2019-2024 - Licensed under MIT
6 */
7
8// Config
9const isOpenClass = 'modal-is-open';
10const openingClass = 'modal-is-opening';
11const closingClass = 'modal-is-closing';
12const scrollbarWidthCssVar = '--pico-scrollbar-width';
13const animationDuration = 400; // ms
14let visibleModal = null;
15
16// Toggle modal
17const toggleModal = (event) => {
18 event.preventDefault();
19 const modal = document.getElementById(event.currentTarget.dataset.target);
20 if (!modal) return;
21 modal && (modal.open ? closeModal(modal) : openModal(modal));
22};
23
24// Open modal
25const openModal = (modal) => {
26 const { documentElement: html } = document;
27 const scrollbarWidth = getScrollbarWidth();
28 if (scrollbarWidth) {
29 html.style.setProperty(scrollbarWidthCssVar, `${scrollbarWidth}px`);
30 }
31 html.classList.add(isOpenClass, openingClass);
32 setTimeout(() => {
33 visibleModal = modal;
34 html.classList.remove(openingClass);
35 }, animationDuration);
36 modal.showModal();
37};
38
39// Close modal
40const closeModal = (modal) => {
41 visibleModal = null;
42 const { documentElement: html } = document;
43 html.classList.add(closingClass);
44 setTimeout(() => {
45 html.classList.remove(closingClass, isOpenClass);
46 html.style.removeProperty(scrollbarWidthCssVar);
47 modal.close();
48 }, animationDuration);
49};
50
51// Close with a click outside
52document.addEventListener('click', (event) => {
53 if (visibleModal === null) return;
54 const modalContent = visibleModal.querySelector('article');
55 const isClickInside = modalContent.contains(event.target);
56 !isClickInside && closeModal(visibleModal);
57});
58
59// Close with Esc key
60document.addEventListener('keydown', (event) => {
61 if (event.key === 'Escape' && visibleModal) {
62 closeModal(visibleModal);
63 }
64});
65
66// Get scrollbar width
67const getScrollbarWidth = () => {
68 const scrollbarWidth =
69 window.innerWidth - document.documentElement.clientWidth;
70 return scrollbarWidth;
71};
72
73// Is scrollbar visible
74const isScrollbarVisible = () => {
75 return document.body.scrollHeight > screen.height;
76};
Given the size of the script, I would have extracted this code in a JS file, but I wanted to keep the logic close to the HTML element triggering it.
This is where Hyperscript comes in.
Hyperscript, the missing piece for homemade SSR π
Difficulties with homemade SSR with HTMX π
When doing a homemade SSR server, the server becomes "fat". It is responsible to render the final HTML, instead of an SPA, where the client renders the HTML.
The motivation for SSR comes simply by a wish to go back to the good old days of the web: the server serves a proper page and the client simply renders the page, without any complexity. This has many advantages, with one being simply the performance. Servers are known to be strong, whereas clients are not, especially modern browsers.
When implementing SSR, most of the time, people will prefer full-stack solutions like NextJS or SvelteKit. But the biggest issue with these frameworks is that they are heavy, and the programming language Typescript doesn't offer enough safety.
This is why, some people will choose to use HTMX, a solution to do SSR by serving data using HTML. For example, instead of serving:
1{
2 "results": [
3 {
4 "title": "My blog article"
5 },
6 {
7 "title": "My second blog article"
8 }
9 ]
10}
HTMX would serve a properly rendered HTML fragment:
1<section>
2 <ul>
3 <li>My blog article</li>
4 <li>My second blog article</li>
5 </ul>
6</section>
This offers these advantages:
- No JS required, which improves performance
- Locality of behavior
- Agnostic to the programming language
- The server is fat, and the client is thin
But has these drawbacks:
- No front-end framework
- And if there are, it requires some compilation (React, Vue, etc.)
- And if they don't require compilation, their performance can be doubtful (see alpine, as it is often the most recommended framework to use with HTMX)
- Styling is complicated
Hyperscript as a simple solution π
Hyperscript is a small library that allows to write simple script alongside the HTML, without disrupting the lisibility of the code.
To install it, simple put in the index.html:
1<script src="https://unpkg.com/hyperscript.org@0.9.14"></script>
Then, you can write:
1<button _="on click send hello to <form />">Send</button>
...I know. This is a new syntax to learn, and honestly, I still have a hard time with it.
Other drawbacks are:
- No compilation, i.e, no compile-time checks.
- The syntax itself isn't clear due to its "almost-English" nature.
- CSP issues (see Security - Hyperscript)
Dialog Modal with Hyperscript and PicoCSS π
Even though Hyperscript has some issues, it is still more clearer than the script above. Here's the implementation of a dialog using Hyperscript:
1<button _="on click call #register-dialog.showModal()">Open dialog</button>
2<dialog id="register-dialog">
3 <article
4 _="on click[#register-dialog.open and event.target.matches('dialog')] from elsewhere call #register-dialog.close()"
5 >
6 <header>
7 <button
8 aria-label="Close"
9 rel="prev"
10 _="on click call #register-dialog.close()"
11 ></button>
12 <p>
13 <strong>ποΈ Thank You for Registering!</strong>
14 </p>
15 </header>
16 <p>
17 We're excited to have you join us for our upcoming event. Please arrive at
18 the museum on time to check in and get started.
19 </p>
20 <ul>
21 <li>Date: Saturday, April 15</li>
22 <li>Time: 10:00am - 12:00pm</li>
23 </ul>
24 </article>
25</dialog>
This is what I'm talking about locality of behavior. Thanks to the syntax of Hyperscript, there is no need to search for an element using verbose functions such as document.getElementById, and the code is more clear and readable. The only thing I need to example is:
1<article
2 _="on click[#register-dialog.open and event.target.matches('dialog')] from elsewhere call #register-dialog.close()"
3></article>
which means:
- I attach an event listener and listen for events only when the dialog is open and the event target is a dialog (the user has clicked on the hitbox of the dialog, this also includes the background).
- If there is an event that is not hitting the
<article>(elsewhereof the<article>), I close the dialog.
Pretty cool, huh?
Conclusion π
This is just a small article about this deadly combination. I know you may simply not like Hyperscript, and would criticize it as an alternative to jQuery or vanilla JS, but I think it's better than them since I can write the code alongside the HTML, and it's more readable.
Also, if the syntax of hyperscript is too difficult, hyperscript is also able to call JS code, which makes it a good extension of HTML.