Invoker Commands are Baseline: the click listeners in your modals and menus are dead code now
webdevelopment September 29, 2026 · Mintec

Invoker Commands are Baseline: the click listeners in your modals and menus are dead code now

The Invoker Commands API (commandfor + command) became Baseline in December 2025, so a button can open a dialog or toggle a popover with no JavaScript at all. Here is the component-by-component framework for what you can delete today, and the two places native still loses.

Invoker Commands are Baseline: the click listeners in your modals and menus are dead code now

If your modal, drawer, menu or tooltip still relies on a JavaScript click listener to open, that listener is now dead code. commandfor and command are Baseline — supported by Chrome, Edge, Firefox and Safari since December 2025 — and the browser will open the dialog, close it, move focus and keep aria-expanded in sync without a single line of your script running.

That is the direct answer. The interesting part is what teams do with it. Baseline is not a feature announcement; it is a deletion deadline. Every month a supported feature stays wrapped in hand-rolled wiring, you are paying maintenance, bundle weight and accessibility risk for something the platform already owns.

We have made this argument before, in a different layer. We migrated projects away from ARIA widgets because native markup beat the roles we were faking, and we wrote up the post-mortems when teams over-built their stack. The wiring layer was always the last holdout, because until now there was no native way to say "this button opens that element." That gap closed in December.

What actually shipped

The Invoker Commands API adds two attributes to <button>:

<button type="button" commandfor="site-menu" command="show-modal">Menu</button>

<dialog id="site-menu">
  <button type="button" commandfor="site-menu" command="close">Close</button>
  <nav><!-- ... --></nav>
</dialog>

That is the whole integration. Top-layer promotion, focus trapping, focus restoration on close, Escape handling and the accessibility bindings all come from <dialog> and the Popover API, which the button now drives declaratively. The built-in vocabulary is small and closed: show-modal, close and request-close for dialogs; show-popover, hide-popover and toggle-popover for popovers.

ComponentNative stackJavaScript that survivesVerdictModal dialog<dialog> + commandfor="show-modal" + closedby="any"None for open, close, backdrop and EscapeDelete the listenersPopover menu, dropdown, tooltip[popover] + commandfor="toggle-popover"None for show, hide, light-dismissDelete the listenersOff-canvas navigation<dialog> + invokersActive-link state onlyDelete the open/close layerAccordion<details> (with name for single-open)None for open and closeNative, but invokers do not target it yetSelect / filter controlappearance: base-select (Customizable Select)None, where supportedProgressive enhancement only — Firefox is not there yetTabs, segmented control, carousel, custom widgetNo native elementBehaviour, state and keyboard handlingKeep the script; delete only the button listeners

Read the last row carefully. Invoker commands do not replace component logic. They replace the wiring between a button and whatever that button controls. For a modal, wiring was the entire job, so the job disappears. For a tab list, wiring was 10% of the job and the other 90% still has to be written and audited.

The four details that decide whether this is an upgrade or a regression

Most coverage stops at the code sample. These are the parts that actually bite in production.

The command event does not bubble. It fires directly on the element named by commandfor, and it does not cross shadow boundaries. If you are used to delegating click listeners from a container, that pattern silently stops working. Attach the listener to the target element.

Only <button> can be an invoker. The attributes and their IDL counterparts live on HTMLButtonElement. Links, inputs and custom elements wrapping a button get nothing for free — the inner native button has to carry the attributes itself.

Custom commands must start with --. That prefix is reserved, so your --expand or --copy can never collide with a built-in the spec adds later. A value that is neither built-in nor ---prefixed is invalid and dispatches nothing, which is a quiet failure worth a test.

The polyfill does not manage ARIA state for custom commands. The invokers-polyfill dispatches the event correctly, but for -- commands it leaves aria-expanded and aria-pressed to you. Native browsers keep those in sync for built-ins. So a team that adds the polyfill "to be safe" can end up with less correct accessibility markup than the no-polyfill version it replaced.

That last point is worth sitting with, because it inverts the usual caution. Google's own modern-web-guidance repository states the Baseline date as 2025-12-12 in one paragraph, then instructs readers in the next that they "MUST use polyfills as fallbacks" for invoker commands and popovers. Both statements are in the same document. When first-party guidance contradicts itself, teams copy whichever half they read first — and a two-year-old habit of polyfilling everything is exactly how a Baseline feature ships with an accessibility regression on top.

Where native still loses

Two places, and you should know both before you start deleting.

Popover and <dialog> are not interchangeable. A popover is lightweight, light-dismissable and non-modal; a dialog is modal, focus-trapped and top-layer. Choosing wrong is an accessibility bug, not a style choice. Scott O'Hara and Adrian Roselli have both written at length about the semantics each one actually gives you — read them before you convert a tooltip into a modal because show-modal looked convenient.

Not everything reached Baseline on the same day. Customizable Select shipped in Chrome and Edge 135 and landed in Safari 27, but Firefox is still experimental. We wrote about what that means for a design system — you style it behind @supports, and the unstyled fallback stays a working <select>. Progressive enhancement, not a drop-in.

There is also a focus quirk worth testing: on some WebKit builds, showModal() leaves a visible focus ring on the first element for pointer users, and clients notice. Test with a real trackpad before you ship.

How we run the audit

The workflow is boring, and it works:

  1. Inventory every element in your project with a click handler attached for show/hide purposes — modal triggers, menu toggles, tooltip pins, drawer openers.
  2. Split them into "opens or closes something native" and "does something else." Step one usually kills two thirds of the list.
  3. For the survivors, confirm the target is a <dialog> or has popover. If it is a <div> with position: fixed, you have a bigger migration than this article covers.
  4. Replace the listener with commandfor + command. Add type="button" so the button never submits a form by accident.
  5. Check what JavaScript remains. If it is zero, delete the handler and the polyfill import with it.
  6. Test keyboard only: Tab in, Enter, Escape, focus return. If any of those were your script's job, they are now the browser's job — confirm it is doing them.

The reason to run this now rather than next quarter is not tidiness. Browsers moved to faster, shorter release cycles this year — we covered Chrome's two-week cycle when it landed — and the same cadence applies to the platform features underneath. The gap between "the spec exists" and "your users' browsers have it" is shorter than it has ever been, which means the gap between "Baseline" and "we still ship a polyfill" is pure waste.

What arrives next

<details> support for invoker commands is still on the roadmap rather than in the spec, so an accordion's open button stays native HTML but not yet declaratively wired. The sibling proposal, interestfor, aims at hover- and focus-triggered behaviour — tooltips that work without script — and is not Baseline.

Neither changes the conclusion. The modal and menu layer, which is where most sites keep the most duplicated listeners, is finished. The question is whether your codebase reflects that.

Frequently Asked Questions

What is the Invoker Commands API?

It is a pair of HTML attributes, commandfor and command, added to the button element. commandfor points at the element the button controls, and command names the action to run, such as show-modal, close, request-close, show-popover, hide-popover or toggle-popover. The browser performs the action, manages focus and updates the accessibility state, so no event listener is required for the built-in actions.

Is the Invoker Commands API supported in all browsers?

Yes. It has been Baseline Newly available since 12 December 2025: Chrome and Edge 135, Firefox 144 and Safari 26.2 all ship it. A polyfill is only justified for audiences stuck on browsers older than those versions, and the popular polyfill does not maintain ARIA state for custom commands, so shipping it can cost you accessibility rather than protect it.

When should a component still use JavaScript instead of invoker commands?

When the behaviour has no built-in command. Tabs, segmented controls, carousels and custom widgets still need script, because the spec defines no state changes for them. Invoker commands help there too, but only as the wiring: your script stays, the click listeners on the buttons do not.

Related Articles