
What Nobody Tells You About the Invoker API: Why Your Next UI Library Might Not Need JavaScript
The new HTML invoker attributes are quietly revolutionizing how we handle UI interactions by moving state management and event listeners directly into the browser's native engine.
Most of the JavaScript you’re shipping today to open menus, toggle sidebars, and show modals is technically technical debt you haven't realized yet. For years, we've accepted a "JS Tax" for the most basic UI interactions, loading entire state management libraries just to change a CSS property from display: none to display: block.
The Invoker API is about to change that. It’s a set of new HTML attributes that let you trigger actions on elements without writing a single line of addEventListener. If you've been following the Popover API, this is the logical, much more powerful next step.
The "Manual" Way is Getting Old
Usually, if you want a button to open a dialog, you do something like this:
const btn = document.querySelector('#open-btn');
const modal = document.querySelector('#my-modal');
btn.addEventListener('click', () => {
modal.showModal();
});It looks harmless. But multiply this by 50 components, add in framework overhead, accessibility concerns (did you remember to link the aria-controls?), and the risk of the JS execution being blocked by a heavy data fetch, and suddenly your UI feels "janky."
Meet commandfor and command
The Invoker API introduces two primary attributes: commandfor and command. You attach them directly to a <button> (or <input type="button">), and the browser handles the rest.
Here is how you open a dialog now:
<button commandfor="my-dialog" command="show-modal">
Open Settings
</button>
<dialog id="my-dialog">
<p>Look, Ma! No JavaScript!</p>
<button commandfor="my-dialog" command="close">Close</button>
</dialog>Why this is better than your React component:
1. Zero Main Thread Blocking: The interaction happens in the browser's native code. Even if your site is busy crunching big data or loading a heavy map, the button remains responsive.
2. Built-in Accessibility: The browser automatically creates the relationship between the trigger and the target.
3. State is Managed by the DOM: You don't need a const [isOpen, setIsOpen] = useState(false) that triggers a re-render of your entire header.
It’s Not Just for Modals
The Invoker API works beautifully with the Popover API, but it also handles videos, details elements, and more.
I found that the most satisfying use case is the "Toggle" pattern. We spend so much time writing logic for sidebars. With the Invoker API, it's a one-liner:
<button commandfor="sidebar" command="toggle-popover">
Toggle Sidebar
</button>
<aside id="sidebar" popover>
<nav>...</nav>
</aside>The command attribute recognizes several built-in actions:
* show-modal / close (for Dialogs)
* toggle-popover / show-popover / hide-popover (for Popovers)
* open / close (for the <details> element)
The "Secret Sauce": Custom Commands
You might be thinking, "This is great for simple stuff, but my app is complex." The spec authors thought of that. You aren't limited to built-in commands. You can define your own.
While you *do* need a tiny bit of JS to define the behavior of a custom command, the Invoker API handles the event delegation and the "wiring" for you.
<button commandfor="counter" command="--increment">
Add One
</button>
<output id="counter">0</output>
<script>
const output = document.querySelector('#counter');
// Listen for the 'command' event on the target element
output.addEventListener('command', (event) => {
if (event.command === '--increment') {
output.value = parseInt(output.value) + 1;
}
});
</script>The beauty here is that the button doesn't need to know *how* to increment the counter; it just knows it's sending a command to that specific ID. This is true separation of concerns.
What Nobody Tells You: The Edge Cases
As much as I love this, it’s not magic. Here are a few things I’ve run into while experimenting:
* Shadow DOM: Just like label for or aria-labelledby, commandfor cannot (yet) easily cross Shadow DOM boundaries. If your button is in one web component and your dialog is in another, you'll still be reaching for the JS toolbox.
* The "Button" Requirement: Commands only work on buttons. Don't try to put a commandfor on a div or an a tag. It’s for semantic buttons only—which is actually a good thing for the web.
* Progressive Enhancement: This is the big one. If the browser doesn't support the Invoker API, the button just... does nothing. You’ll want to check for support and fall back to manual listeners for the time being.
if (!HTMLButtonElement.prototype.hasOwnProperty('commandForElement')) {
// Fallback to manual event listeners
}Why Your Next UI Library Might Not Need JS
We are moving toward a "Lightweight Web." We spent the last decade moving everything into JavaScript because the browser was slow to evolve. Now, the browser is catching up.
Between CSS Container Queries, the Popover API, Native Masonry, and now the Invoker API, the amount of "glue code" we need to write is plummeting. Your next UI library shouldn't be a 50kb runtime; it should be a collection of smart HTML patterns that leverage what the browser already knows how to do.
Stop writing onClick handlers for things the browser can do natively. Give the Invoker API a spin in Chrome Canary or the latest Edge, and see how much code you can actually delete. It’s surprisingly therapeutic.

