Invokers Polyfill: Buttons That Just Know What to Do
Imagine a world where a button knows exactly which dialog to open, which popover to toggle, and which panel to close -- all without a single line of JavaScript. That world already exists in modern browsers thanks to the Invoker Commands API, a set of HTML attributes (commandfor and command) that let you wire interactive behavior directly into your markup. The catch is that not every browser your users run has caught up. That is where invokers-polyfill steps in. Built by Keith Cirkel, a web standards contributor who has shipped patches to browsers and helped shape the spec itself, this zero-dependency polyfill faithfully reproduces the Invoker Commands API for older browsers so you can write declarative HTML today and trust that it works everywhere.
Why Declarative Beats Imperative
For years, the standard recipe for opening a dialog from a button looked like this: find the button, find the dialog, add an event listener, call showModal(). Multiply that by every interactive pattern on your page and you end up with a lot of glue code that exists only to connect point A to point B. The Invoker Commands specification replaces all of that with two attributes:
commandfor-- points to theidof the target elementcommand-- names the action to perform on that element
The browser handles the rest. No query selectors, no event listeners, no wiring. The polyfill preserves this exact contract for browsers that do not yet support the spec natively.
What the Polyfill Brings to the Table
Beyond simply shimming the two attributes, invokers-polyfill delivers a set of capabilities that make it production-worthy:
- Full spec alignment -- supports every built-in command defined in the current specification, including
show-modal,close,request-close,toggle-popover,open-popover, andclose-popover - Custom commands -- lets you define your own actions using the double-dash prefix convention (
--my-action), exactly as the spec prescribes - CommandEvent dispatch -- fires real
CommandEventobjects on target elements so your event listeners work identically to native behavior - Progressive enhancement -- automatically detects native support and skips itself when the browser already handles everything
- Zero dependencies -- nothing to audit, nothing to conflict, nothing to slow down your bundle
- Async-safe loading -- can be loaded with
asyncon a script tag without blocking page render
Getting It Installed
Add invokers-polyfill to your project with your preferred package manager:
npm install invokers-polyfill
yarn add invokers-polyfill
If you prefer a CDN approach, drop a script tag into your HTML and you are done:
<script
type="module"
async
src="https://unpkg.com/invokers-polyfill@latest/invoker.min.js"
></script>
Opening Doors with Declarative Dialogs
The One-Line Modal
The simplest use case is opening a <dialog> as a modal. In plain HTML with the polyfill loaded, this is all you need:
<button commandfor="signup-dialog" command="show-modal">
Sign Up
</button>
<dialog id="signup-dialog">
<h2>Create Your Account</h2>
<form method="dialog">
<label>
Email
<input type="email" name="email" required />
</label>
<button type="submit">Submit</button>
<button commandfor="signup-dialog" command="close">Cancel</button>
</form>
</dialog>
No JavaScript at all. The "Sign Up" button opens the dialog in modal mode, and the "Cancel" button closes it. The polyfill intercepts click events on buttons with commandfor and command attributes, looks up the target element, and calls the corresponding native method -- showModal() or close() -- on your behalf.
Toggling Popovers Without Ceremony
Popovers follow the same pattern. Declare the target, declare the action, and the polyfill handles the plumbing:
<button commandfor="user-menu" command="toggle-popover">
Account
</button>
<div id="user-menu" popover>
<nav>
<ul>
<li><a href="/profile">Profile</a></li>
<li><a href="/settings">Settings</a></li>
<li><a href="/logout">Log Out</a></li>
</ul>
</nav>
</div>
You also have fine-grained control with open-popover and close-popover if toggling is not what you want. These map directly to the native showPopover() and hidePopover() methods.
Conditional Import for the Cautious
If you only want the polyfill to load when the browser actually needs it, import from the fn subpath and check support manually:
import { isSupported, apply } from "invokers-polyfill/fn";
if (!isSupported()) {
apply();
}
This is the recommended pattern for production builds. Browsers that already implement Invoker Commands natively will skip the polyfill entirely, keeping your runtime footprint at zero in modern environments.
Beyond the Basics
Guarding Unsaved Work with request-close
The request-close command is a thoughtful addition to the spec. Instead of closing a dialog immediately, it dispatches a cancelable cancel event on the dialog, giving you a chance to intervene:
<button commandfor="editor-dialog" command="show-modal">
Edit Document
</button>
<dialog id="editor-dialog">
<form>
<textarea id="doc-content" rows="10" cols="40"></textarea>
<div>
<button commandfor="editor-dialog" command="request-close">
Discard
</button>
<button type="submit">Save</button>
</div>
</form>
</dialog>
const dialog = document.getElementById("editor-dialog") as HTMLDialogElement;
const textarea = document.getElementById("doc-content") as HTMLTextAreaElement;
let savedContent = "";
dialog.addEventListener("cancel", (event: Event) => {
if (textarea.value !== savedContent) {
event.preventDefault();
const confirmed = window.confirm(
"You have unsaved changes. Discard them?"
);
if (confirmed) {
textarea.value = savedContent;
dialog.close();
}
}
});
The request-close command triggers the cancel event. If you call preventDefault(), the dialog stays open. This pattern is perfect for forms with unsaved data, confirmation workflows, or any scenario where closing should be conditional.
Custom Commands for Your Own Vocabulary
The spec reserves non-prefixed command names for built-in browser actions, but anything prefixed with -- is yours to define. This opens the door to building your own declarative interaction vocabulary:
<button commandfor="notification-panel" command="--slide-in">
Show Notifications
</button>
<aside id="notification-panel" class="panel-hidden">
<h2>Notifications</h2>
<ul>
<li>You have 3 new messages</li>
<li>Deployment succeeded</li>
</ul>
<button commandfor="notification-panel" command="--slide-out">
Dismiss
</button>
</aside>
const panel = document.getElementById("notification-panel") as HTMLElement;
panel.addEventListener("command", (event: Event) => {
const commandEvent = event as CustomEvent & { command: string };
if (commandEvent.command === "--slide-in") {
panel.classList.remove("panel-hidden");
panel.classList.add("panel-visible");
}
if (commandEvent.command === "--slide-out") {
panel.classList.add("panel-hidden");
panel.classList.remove("panel-visible");
}
});
The polyfill dispatches a CommandEvent on the target element with the command property set to your custom name. You listen for it just like any other DOM event. This keeps the declarative pattern intact -- the HTML says what should happen, and a single event listener on the target decides how.
Accessibility: Your Responsibility
One important caveat: the polyfill does not automatically manage ARIA attributes the way native browser implementations do. When browsers handle commandfor natively, they can set aria-expanded, aria-controls, and other states behind the scenes. The polyfill cannot replicate that browser-level integration, so you need to handle it yourself:
const toggleButton = document.querySelector(
'[commandfor="user-menu"]'
) as HTMLButtonElement;
const menu = document.getElementById("user-menu") as HTMLElement;
toggleButton.setAttribute("aria-controls", "user-menu");
toggleButton.setAttribute("aria-expanded", "false");
menu.addEventListener("command", () => {
const isExpanded = toggleButton.getAttribute("aria-expanded") === "true";
toggleButton.setAttribute("aria-expanded", String(!isExpanded));
});
This is not optional if you care about your users. Screen readers rely on these attributes to communicate state changes, and skipping them means your interactive elements become invisible to assistive technology.
Where This Polyfill Fits in 2026
As of early 2026, Invoker Commands have reached baseline support across all major browsers -- Chrome 135+, Firefox 144+, and Safari 26.2+. That is a significant milestone that means most users on current browsers do not need the polyfill at all. But "most" is not "all." Enterprise environments, older devices, and the long tail of browser versions mean the polyfill still earns its keep as a backwards-compatibility layer.
The recommended strategy is straightforward: use the conditional import pattern, write your HTML with commandfor and command as if every browser supports them, and let the polyfill quietly fill the gaps where needed. When your analytics tell you that legacy browsers have fallen below your support threshold, removing the polyfill is a one-line change -- delete the import. Your HTML stays exactly the same.
Wrapping Up
invokers-polyfill is a small library solving a specific problem well. It brings the Invoker Commands API to browsers that lack native support, faithfully reproducing the spec's behavior with zero dependencies and minimal overhead. The real value is not the polyfill itself but the pattern it enables: declarative, readable HTML where buttons state their intent directly in the markup instead of hiding it in JavaScript event handlers buried three files deep.
Whether you are building a new project and want to use Invoker Commands today with full backwards compatibility, or migrating an existing codebase away from manual event listener boilerplate, invokers-polyfill makes the transition painless. Write the HTML the way the spec intended, let the polyfill handle the rest, and enjoy the moment when you delete it because you no longer need it.