For years, making interactive elements work on the web required reaching for JavaScript almost immediately. Opening a modal dialog, revealing a popover, or toggling collapsible content meant finding the trigger, querying the target element in the DOM, attaching event listeners, and managing accessibility states manually.
The Invoker Commands API introduces a declarative, browser-native way to wire buttons directly to target elements using two HTML attributes: commandfor and command.
By pushing common interactions into HTML markup, basic UI controls become functional before client scripts even download. For behaviors that do require business logic, custom commands streamline event routing without cluttering elements with fragile data-* attributes.
How It Works
commandfor: Placed on a<button>, this attribute takes theidof the target element it should control.command: Specifies the action to trigger. Built-in commands include actions likeshow-modalorcloseon<dialog>elements, andshow-popover,hide-popover, ortoggle-popoveron popovers.- Custom Commands (
--*): Any command starting with double hyphens (e.g.,--expand,--reset) dispatches a nativeCommandEventto the target element. Instead of attaching listeners to multiple triggers, you attach a singlecommandlistener to the target.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>
Invoker Commands API Demo</title>
<link href="style.css" rel="stylesheet" />
<script src="invokerCommands_API.js" defer></script>
</head>
<body>
<div class="demo-wrapper">
<!-- Section 1: Pure Declarative Modal (No JS Required) -->
<section class="demo-card">
<h2>Native Dialog Control</h2>
<p>Open and close this dialog declaratively using built-in commands.</p>
<button commandfor="site-dialog" command="show-modal" class="btn primary">
Open Dialog
</button>
</section>
<!-- The Target Dialog -->
<dialog id="site-dialog" class="custom-dialog">
<div class="dialog-content">
<h3>Invoker Commands Demo</h3>
<p>This modal was opened without a single line of JavaScript.</p>
<button commandfor="site-dialog" command="close" class="btn secondary">
Close Dialog
</button>
</div>
</dialog>
<!-- Section 2: Custom Command Event Target -->
<section class="demo-card">
<h2>Custom Invoker Commands</h2>
<p>Target listens for native <code>command</code> events via custom actions.</p>
<div class="button-group">
<!-- Buttons invoke actions on the target container -->
<button commandfor="status-panel" command="--set-active" class="btn">
Set Active
</button>
<button commandfor="status-panel" command="--reset" class="btn secondary">
Reset
</button>
</div>
<!-- Target Element receiving the commands -->
<div id="status-panel" class="status-panel">
Status: <span id="status-indicator">Idle</span>
</div>
</section>
</div>
</body>
</html>
:root {
--font-base: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
--color-bg: #f8fafc;
--color-surface: #ffffff;
--color-text: #1e293b;
--color-primary: #0284c7;
--color-primary-hover: #0369a1;
--color-border: #cbd5e1;
--radius: 8px;
}
body {
font-family: var(--font-base);
background-color: var(--color-bg);
color: var(--color-text);
padding: 2rem;
margin: 0;
}
.demo-wrapper {
max-width: 600px;
margin: 0 auto;
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.demo-card {
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: var(--radius);
padding: 1.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
}
.demo-card h2 {
margin-top: 0;
font-size: 1.25rem;
}
.button-group {
display: flex;
gap: 0.75rem;
margin-bottom: 1rem;
}
.btn {
font-family: inherit;
font-size: 0.9rem;
font-weight: 500;
padding: 0.6rem 1rem;
border: 1px solid transparent;
border-radius: 6px;
cursor: pointer;
background-color: #e2e8f0;
color: #334155;
transition: background-color 0.15s ease;
}
.btn:hover {
background-color: #cbd5e1;
}
.btn.primary {
background-color: var(--color-primary);
color: #ffffff;
}
.btn.primary:hover {
background-color: var(--color-primary-hover);
}
.btn.secondary {
border-color: var(--color-border);
background-color: transparent;
}
.btn.secondary:hover {
background-color: #f1f5f9;
}
/* Native Dialog Styling */
.custom-dialog {
border: 1px solid var(--color-border);
border-radius: var(--radius);
padding: 2rem;
box-shadow: 0 10px 25px -5px rgba(0, 0, 0, 0.1);
max-width: 400px;
}
.custom-dialog::backdrop {
background: rgba(15, 23, 42, 0.45);
backdrop-filter: blur(2px);
}
/* Custom Status Target */
.status-panel {
padding: 1rem;
border: 1px dashed var(--color-border);
border-radius: 6px;
font-weight: 600;
background-color: #f8fafc;
transition: all 0.2s ease;
}
.status-panel.active {
background-color: #f0fdf4;
border-color: #86efac;
color: #166534;
}
// Step 1: Query the target element that will receive the dispatched commands.
// In the Invoker Commands API, buttons point to this element using 'commandfor="status-panel"'.
const statusPanel = document.querySelector('#status-panel');
// Step 2: Grab the internal text span so we can update the displayed state on screen.
const statusIndicator = document.querySelector('#status-indicator');
// Step 3: Attach a single listener for the native 'command' event directly to the target element.
// Buttons with 'commandfor' pointing to this ID will automatically dispatch this event here.
statusPanel.addEventListener('command', (event) => {
// Extract the custom command name from the CommandEvent object.
// Custom commands are required to start with two dashes (e.g., '--set-active', '--reset').
const { command } = event;
// The event object also provides 'event.source', referencing the exact <button> that triggered it.
// console.log("Invoked by button:", event.source);
// Step 4: Route the incoming action in one centralized location rather than managing multiple button listeners.
switch (command) {
case '--set-active':
// Add the active CSS modifier class to update panel styling.
statusPanel.classList.add('active');
// Update the readable text inside the indicator container.
statusIndicator.textContent = 'Active (Command Handled)';
break;
case '--reset':
// Strip the active styling class back to default resting state.
statusPanel.classList.remove('active');
// Revert the text label back to its idle baseline.
statusIndicator.textContent = 'Idle';
break;
default:
// Gracefully ignore or log any custom commands that aren't handled by this component.
break;
}
});
Key Takeaways for Developers
- Progressive Enhancement: Built-in modal and popover behaviors work reliably right away without loading custom script files first.
- Cleaner DOM Architecture: Decouples event triggers from target elements cleanly in HTML without creating ad-hoc
onclickhandlers or manual selector lookups. - Centralized Handling: Custom commands flip the standard pattern: buttons dispatch commands to the component, so you listen on the element that owns state rather than binding individual triggers.





