What if I told you that you could open a modal, close it, and control a popover menu using HTML alone?
No addEventListener().
No onclick handlers.
No JavaScript functions just to open and close things.
Modern HTML supports Invoker Commands, using two attributes:
commandforcommand
And they're surprisingly powerful.
In this tutorial, we'll explore them by building interactive features inside a real luxury real estate website called HAVEN Estates.
Instead of demonstrating everything with empty boxes, we'll use actual property cards, detailed property dialogs, a navigation popover, and a private-viewing form.
Prefer watching rather than reading? Watch the full step-by-step YouTube tutorial.
If you want to go deeper and build full responsive layouts faster, check out my guide: Build Any Responsive Layout with CSS Grid & Flexbox. It walks through building real pages component by component.
Let's get started.
1. The old way: JavaScript event handlers
Imagine you're building a website with a button that opens a modal.
Traditionally, you might write:
<button id="open-dialog">
View Property
</button>
<dialog id="property-dialog">
<h2>Villa Sereno</h2>
<p>Explore this luxury residence.</p>
</dialog>
Then use JavaScript:
const openButton = document.querySelector("#open-dialog");
const dialog = document.querySelector("#property-dialog");
openButton.addEventListener("click", () => {
dialog.showModal();
});
This works, and JavaScript is still essential when you need custom application logic. But for a simple built-in interaction like opening a dialog, we no longer need to write this event handler in supporting browsers.
2. Meet commandfor and command
Here's the HTML-only alternative:
<button
type="button"
commandfor="property-dialog"
command="show-modal"
>
View Property
</button>
<dialog id="property-dialog">
<h2>Villa Sereno</h2>
<p>Explore this luxury residence.</p>
</dialog>
That's it. The browser knows which dialog to open and what action to perform.
commandfor = WHERE
The commandfor attribute identifies the target element:
commandfor="property-dialog"
This connects the button to the element with the matching ID:
<dialog id="property-dialog">
Think of it as answering: Which element should this button control?
command = WHAT
The command attribute tells the browser what action to perform:
command="show-modal"
Think of it as answering: What should happen to that element?
The easiest way to remember the difference:
commandfor= WHERE
command= WHAT
That's the foundation of HTML Invoker Commands.
3. The commands you need to know
For built-in dialogs and popovers, common command values include:
| Command | Purpose |
|---|---|
show-modal |
Open a dialog as a modal |
close |
Close a dialog |
request-close |
Request a dialog to close |
show-popover |
Show a popover |
hide-popover |
Hide a popover |
toggle-popover |
Toggle a popover |
Some values target <dialog> elements, while others target elements with the popover attribute. That distinction is important.
Let's use them in our actual project.
4. Build a property dialog in HAVEN Estates
HAVEN Estates is a premium property website featuring luxury residential listings. Each property card contains a quick-view button.
Our goal is simple: when a visitor clicks a property button, a dialog should open with more information.
Let's use one of the properties: Villa Sereno.
Step 1: Create the button
Inside our property card, we have a button similar to this:
<button
class="image-action"
type="button"
commandfor="property-sereno"
command="show-modal"
aria-label="Quick view of Villa Sereno"
>
↗
</button>
Notice three important things:
- It's a real
<button>. - The button points to
property-sereno. - The command is
show-modal.
Now we need to create the target.
Step 2: Add the dialog
<dialog
class="modal property-dialog"
id="property-sereno"
aria-labelledby="dialog-title-sereno"
>
<div class="modal-body">
<h2 id="dialog-title-sereno">
Villa Sereno
</h2>
<p>
A luxury Mediterranean residence
designed for exceptional coastal living.
</p>
<button
type="button"
commandfor="property-sereno"
command="close"
>
Close
</button>
</div>
</dialog>
The id matches the button's commandfor value. That's what makes the connection work.
Click the quick-view button, and the browser opens the dialog—without a custom click handler or a JavaScript function to call showModal().
Step 3: Close the dialog
Look at our close button:
<button
type="button"
commandfor="property-sereno"
command="close"
>
Close
</button>
We target the same dialog, but change the command from show-modal to close.
What about request-close?
Our HAVEN project also uses this pattern for the close icon:
<button
type="button"
commandfor="property-sereno"
command="request-close"
aria-label="Close property details"
>
×
</button>
The difference matters:
-
closedirectly closes the dialog. -
request-closerequests closure through the dialog's cancelable close-request behavior.
That can be useful when an application needs to intercept a closing request, such as warning about unsaved changes. For our simple property preview, both normally result in a closed dialog.
5. Style the dialog with CSS
Native HTML controls don't mean we have to accept boring designs.
The HAVEN Estates dialogs use custom styling, property imagery, elegant typography, and a backdrop. Here's a simplified example:
.modal {
width: min(90vw, 760px);
max-height: 90dvh;
padding: 0;
border: 0;
border-radius: 18px;
overflow: auto;
background: #fff;
color: #24211d;
box-shadow: 0 24px 80px rgba(0, 0, 0, 0.25);
}
.modal::backdrop {
background: rgba(10, 14, 18, 0.65);
backdrop-filter: blur(6px);
}
The ::backdrop pseudo-element styles the layer behind a modal dialog. And max-height: 90dvh limits the dialog to the dynamic viewport height, with scrolling available for longer content.
Native behavior and custom design can work together.
6. Create a navigation menu with the Popover API
Now let's move to another real feature in HAVEN Estates: the navigation button.
Clicking it reveals an additional panel containing links such as:
- Discover properties
- Our philosophy
- Exceptional amenities
- Client stories
- Contact us
The panel also has a Book a private viewing button.
We can build this interaction with HTML commands and the Popover API.
Step 1: Create the trigger
<button
class="menu-trigger"
type="button"
commandfor="site-menu"
command="toggle-popover"
aria-label="Open navigation"
>
☰
</button>
This time our command is toggle-popover, and the target is site-menu.
Step 2: Create the popover
<nav
id="site-menu"
class="site-menu"
popover
aria-label="Additional navigation"
>
<div class="menu-top">
<strong>Explore HAVEN</strong>
<button
type="button"
commandfor="site-menu"
command="hide-popover"
aria-label="Close menu"
>
×
</button>
</div>
<a href="#properties">Discover properties</a>
<a href="#story">Our philosophy</a>
<a href="#amenities">Exceptional amenities</a>
<a href="#testimonials">Client stories</a>
<a href="#contact">Contact us</a>
</nav>
The important addition is the popover attribute.
Because we're using the default automatic popover behavior, the browser provides useful built-in interactions:
- The command button can toggle the popover.
- Escape can dismiss the open popover.
- Clicking outside can dismiss it.
- The open popover is placed in the top layer.
All without custom JavaScript for those behaviors.
However, a popover isn't automatically a modal dialog: it does not make the rest of the page inert or trap keyboard focus.
7. Add a viewing form without an open-modal handler
Another feature in HAVEN Estates is the private-viewing form. When a visitor clicks Schedule a viewing, a modal dialog opens.
The trigger uses the same pattern:
<button
type="button"
commandfor="viewing-dialog"
command="show-modal"
>
Schedule a viewing
</button>
And the target is:
<dialog id="viewing-dialog">
<h2>Schedule a viewing</h2>
<form method="dialog">
<label for="full-name">Full name</label>
<input id="full-name" name="name" required>
<label for="email">Email address</label>
<input id="email" name="email" type="email" required>
<button type="submit">Close form preview</button>
</form>
</dialog>
Here's another useful native HTML feature:
<form method="dialog">
When submitted through its submit button, this form can close its containing dialog rather than sending a normal network request. Do not confuse this behavior with saving a booking.
Important: In HAVEN Estates, this is only a frontend demonstration. The form does not actually send or store a viewing request. A production booking system would need server-side handling, validation, data storage, and user feedback.
8. Dialog vs. Popover: When should you use each?
Both are useful, but they solve different problems.
| Feature | Modal dialog | Auto popover |
|---|---|---|
| Typical use | Important focused interaction | Dismissible supplementary UI |
| Makes background inert | Yes, when opened modally | No |
| Escape dismissal | Supported | Supported |
| Outside-click dismissal | Not automatic by default | Supported |
| Top-layer rendering | Yes, when modal | Yes, when shown |
| Example in HAVEN | Property preview, viewing form | Additional navigation |
For HAVEN Estates, we use modal dialogs for detailed property previews and the viewing form, and a popover for additional navigation.
Choosing the right element improves the interaction before we even start styling it.
9. Common mistakes to avoid
Mistake 1: The IDs don't match
Incorrect:
<button commandfor="my-dialog" command="show-modal">
Open
</button>
<dialog id="another-dialog"></dialog>
The button's target doesn't exist under that ID.
Correct:
<button commandfor="my-dialog" command="show-modal">
Open
</button>
<dialog id="my-dialog"></dialog>
Mistake 2: Using the wrong command
These commands are not interchangeable:
command="show-modal"
is intended for a dialog, while:
command="toggle-popover"
is intended for a popover.
Mistake 3: Forgetting accessible names
A button containing only an icon can be difficult to understand without an accessible name.
Instead of:
<button commandfor="property-sereno" command="show-modal">
↗
</button>
Use:
<button
commandfor="property-sereno"
command="show-modal"
aria-label="Quick view of Villa Sereno"
>
↗
</button>
Now assistive technologies can communicate its purpose.
Mistake 4: Assuming modern HTML works in every browser
Invoker Commands are modern platform features. Before using them in production, verify the browser versions your audience needs. Plan a fallback if unsupported browsers are part of your requirements.
Mistake 5: Confusing UI behavior with application functionality
Opening a booking form without JavaScript is not the same as implementing a booking system. Real business processes may still require JavaScript, a backend, authentication, persistence, and validation.
Watch the complete HAVEN Estates project
In the full video tutorial, I demonstrate:
- The completed luxury real estate interface.
- The starter project before commands are added.
- How to connect buttons to property dialogs.
- How to open and close dialogs without custom JavaScript handlers.
- How to build the navigation popover.
- How to control the viewing dialog.
- How to test the finished interactions.
Watch the complete YouTube tutorial:
Watch the HAVEN Estates HTML Commands tutorial
Improve your responsive CSS skills
If you want to become better at building polished layouts like HAVEN Estates, check out my guide:
Build Any Responsive Layout with CSS Grid & Flexbox
Final thoughts
Modern HTML keeps getting more powerful.
The command and commandfor attributes show how browsers can handle common interface interactions without requiring custom event listeners for every button.
That doesn't mean JavaScript is obsolete. It means we can use the browser's native capabilities first and add JavaScript when our application genuinely needs it.
Have you tried HTML Invoker Commands yet?
Would you use them for dialogs and popovers in your next project?
Let me know in the comments.
Happy coding!
— CodeWithDivine
Top comments (0)