Show HN: Invokers: A Polyfill and Extension for HTML Invoker Commands
1 month ago
6
Write Interactive HTML Without Writing JavaScript
Invokers lets you write future-proof HTML interactions without custom JavaScript. It's a polyfill for the upcoming HTML Invoker Commands API and Interest Invokers (hover cards, tooltips), with a comprehensive set of extended commands automatically included for real-world needs like toggling, fetching, media controls, and complex workflow chaining.
✅ Standards-First: Built on the W3C/WHATWG command attribute and Interest Invokers proposals. Learn future-proof skills, not framework-specific APIs.
🧩 Polyfill & Superset: Provides the standard APIs in all modern browsers and extends them with a rich set of custom commands.
✍️ Declarative & Readable: Describe what you want to happen in your HTML, not how in JavaScript. Create UIs that are self-documenting.
🔗 Universal Command Chaining: Chain any command with any other using data-and-then attributes or declarative <and-then> elements for complex workflows.
🎯 Conditional Execution: Execute different command sequences based on success/error states with built-in conditional logic.
🔄 Lifecycle Management: Control command execution with states like once, disabled, and completed for sophisticated interaction patterns.
♿ Accessible by Design: Automatically manages aria-* attributes and focus behavior, guiding you to build inclusive interfaces.
🌐 Server-Interactive: Fetch content and update the DOM without a page reload using simple, declarative HTML attributes.
💡 Interest Invokers: Create hover cards, tooltips, and rich hints that work across mouse, keyboard, and touch with the interestfor attribute.
🚀 Zero Dependencies & Tiny: A featherlight addition to any project, framework-agnostic, and ready to use in seconds.
🎨 View Transitions: Built-in, automatic support for the View Transition API for beautiful, animated UI changes with zero JS configuration.
See Invokers in action with this copy-paste example:
<script type="module" src="https://esm.sh/invokers/compatible"></script>
</head>
<body>
<button type="button" command="--toggle" commandfor="nav-menu" aria-expanded="false">
Menu
</button>
<nav id="nav-menu" hidden>
<a href="/home">Home</a>
<a href="/about">About</a>
<button type="button" command="--hide" commandfor="nav-menu">✕</button>
</nav>
<a href="/profile" interestfor="profile-hint">@username</a>
<div id="profile-hint" popover="hint">
<strong>John Doe</strong><br>
Software Developer<br>
📍 San Francisco
</div>
</body>
</html>'><!DOCTYPE html><html><head><!-- Add Invokers via CDN (includes all commands) --><scripttype="module" src="https://esm.sh/invokers/compatible"></script></head><body><!-- Toggle a navigation menu with zero JavaScript --><buttontype="button" command="--toggle" commandfor="nav-menu" aria-expanded="false">
Menu
</button><navid="nav-menu" hidden><ahref="/home">Home</a><ahref="/about">About</a><!-- Dismiss button that hides itself --><buttontype="button" command="--hide" commandfor="nav-menu">✕</button></nav><!-- Hover cards work automatically with Interest Invokers --><ahref="/profile" interestfor="profile-hint">@username</a><divid="profile-hint" popover="hint"><strong>John Doe</strong><br>
Software Developer<br>
📍 San Francisco
</div></body></html>
That's it! No event listeners, no DOM queries, no state management. The HTML describes the behavior, and Invokers makes it work.
🌐 Platform Proposals & Standards Alignment
Invokers is built on emerging web platform proposals from the OpenUI Community Group and WHATWG, providing a polyfill today for features that will become native browser APIs tomorrow. This section explains the underlying standards and how Invokers extends them.
HTML Invoker Commands API
The Invoker Commands API is a W3C/WHATWG proposal that introduces the command and commandfor attributes to HTML <button> elements. This allows buttons to declaratively trigger actions on other elements without JavaScript.
command attribute: Specifies the action to perform (e.g., show-modal, toggle-popover)
commandfor attribute: References the target element by ID
CommandEvent: Dispatched on the target element when the button is activated
Built-in commands: Native browser behaviors for dialogs and popovers
Advanced Event Triggers: Adds command-on attribute for any DOM event (click, input, submit, etc.)
Expression Engine: Adds {{...}} syntax for dynamic command parameters
Command Chaining: Adds <and-then> elements and data-and-then attributes for workflow orchestration
Conditional Logic: Adds success/error state handling with data-after-success/data-after-error
Lifecycle States: Adds once, disabled, completed states for sophisticated interactions
Interest Invokers (Hover Cards & Tooltips)
The Interest Invokers proposal introduces the interestfor attribute for creating accessible hover cards, tooltips, and preview popovers that work across all input modalities.
interestfor attribute: Connects interactive elements to hovercard/popover content
Multi-modal Support: Works with mouse hover, keyboard focus, and touchscreen long-press
Automatic Accessibility: Manages ARIA attributes and focus behavior
Delay Controls: CSS properties for customizing show/hide timing
Pseudo-classes: :interest-source and :interest-target for styling
Invokers includes a complete polyfill for Interest Invokers with additional enhancements:
Extended Element Support: Works on all HTMLElement types (spec currently limits to specific elements)
Touchscreen Context Menu Integration: Adds "Show Details" item to existing long-press menus
Advanced Delay Controls: Full support for interest-delay-start/interest-delay-end CSS properties
Pseudo-class Support: Implements :interest-source and :interest-target pseudo-classes
Combined Usage: Works seamlessly with Invoker Commands on the same elements
Invokers has deep integration with the Popover API, automatically handling popover lifecycle and accessibility when using popover attributes.
Popover Commands: toggle-popover, show-popover, hide-popover work natively
ARIA Management: Automatic aria-expanded and aria-details attributes
Focus Management: Proper focus restoration when popovers close
Top Layer Integration: Works with the browser's top layer stacking context
Standards Compliance & Future-Proofing
Chrome/Edge: Full Invoker Commands support (v120+)
Firefox: Partial support, actively developing
Safari: Under consideration
Polyfill Coverage: Invokers provides complete fallback for all browsers
Invoker Commands: Graduated from OpenUI, in WHATWG HTML specification
Interest Invokers: Active proposal, expected to graduate soon
Popover API: Already shipping in major browsers
As browsers implement these features natively:
Invokers will automatically detect native support
Polyfill behaviors will gracefully disable
Your HTML markup remains unchanged
Enhanced features (chaining, expressions) continue to work
Why Invokers vs. Native-Only
While waiting for universal browser support, Invokers provides:
Immediate Availability: Use these features today in any browser
Enhanced Functionality: Command chaining, expressions, and advanced workflows
Backward Compatibility: Works alongside native implementations
Progressive Enhancement: Adds features without breaking existing code
This standards-first approach ensures your code is future-proof while providing powerful enhancements that complement the core platform proposals.
Invokers is designed to feel like a natural extension of HTML, focusing on client-side interactions and aligning with future web standards. Here’s how its philosophy and approach differ from other popular libraries.
HTMX makes your server the star; Invokers makes your browser the star.
HTMX is a hypermedia-driven library where interactions typically involve a network request to a server, which returns HTML. Invokers is client-centric, designed to create rich UI interactions directly in the browser, often without any network requests or custom JavaScript.
A user clicks "Edit" to change a name, then "Save" or "Cancel".
HTMX: Server-Driven Swapping
HTMX replaces a div with a form fragment fetched from the server. The entire state transition is managed by server responses.
<div id="user-1" hx-target="this" hx-swap="outerHTML">
<strong>Jane Doe</strong>
<button hx-get="/edit-form/1" class="btn">
Edit
</button>
</div>
Save
Cancel
-->'><!-- HTMX requires a server to serve the edit-form fragment --><divid="user-1" hx-target="this" hx-swap="outerHTML"><strong>Jane Doe</strong><buttonhx-get="/edit-form/1" class="btn">
Edit
</button></div><!-- On click, the server returns this HTML fragment: --><!-- <form hx-put="/user/1"> <input name="name" value="Jane Doe"> <button type="submit">Save</button> <button hx-get="/user/1">Cancel</button> </form> -->
Invokers: Client-Side State Toggling (No JS, No Server)
Invokers handles this by toggling the visibility of two divs that already exist on the page. It's instantaneous and requires zero network latency or server-side logic for the UI change.
<div class="user-profile">
<div id="user-view">
<strong>Jane Doe</strong>
<button type="button" class="btn"
command="--hide" commandfor="user-view"
data-and-then="--show" data-and-then-commandfor="user-edit">
Edit
</button>
</div>
<div id="user-edit" hidden>
<input type="text" value="Jane Doe">
<button type="button" class="btn-primary" command="--emit:save-user:1">Save</button>
<button type="button" class="btn"
command="--hide" commandfor="user-edit"
data-and-then="--show" data-and-then-commandfor="user-view">
Cancel
</button>
</div>
</div>'><!-- Invokers handles this entirely on the client, no server needed --><divclass="user-profile"><!-- 1. The view state (visible by default) --><divid="user-view"><strong>Jane Doe</strong><buttontype="button" class="btn"
command="--hide" commandfor="user-view"
data-and-then="--show" data-and-then-commandfor="user-edit">
Edit
</button></div><!-- 2. The edit state (hidden by default) --><divid="user-edit" hidden><inputtype="text" value="Jane Doe"><buttontype="button" class="btn-primary" command="--emit:save-user:1">Save</button><buttontype="button" class="btn"
command="--hide" commandfor="user-edit"
data-and-then="--show" data-and-then-commandfor="user-view">
Cancel
</button></div></div>
Use Case: Dynamic Content Swapping & Fetching
Replace page sections with new content, either from templates or remote APIs, with precise control over insertion strategy.
HTMX: Server-Driven Content Swapping
HTMX fetches HTML fragments from the server and swaps them into the DOM using hx-swap strategies.
<div id="content-area">
<button hx-get="/api/widget-a" hx-swap="innerHTML">Load Widget A</button>
<button hx-get="/api/widget-b" hx-swap="outerHTML" hx-target="#content-area">Replace Container</button>
</div>
'><!-- HTMX requires server endpoints for each content type --><divid="content-area"><buttonhx-get="/api/widget-a" hx-swap="innerHTML">Load Widget A</button><buttonhx-get="/api/widget-b" hx-swap="outerHTML" hx-target="#content-area">Replace Container</button></div><!-- Server must return complete HTML fragments -->
Invokers: Client-Side DOM Swapping & Fetching
Invokers can swap content from local templates or fetch from APIs, with granular control over insertion strategies.
<template id="widget-a-template">
<div class="widget widget-a">
<h3>Widget A</h3>
<p>This content comes from a local template.</p>
</div>
</template>
<template id="widget-b-template">
<div class="widget widget-b">
<h3>Widget B</h3>
<p>This replaces the entire container.</p>
</div>
</template>
<div id="content-area">
<button command="--dom:swap" data-template-id="widget-a-template"
commandfor="#content-area" data-replace-strategy="innerHTML">
Load Widget A (Inner)
</button>
<button command="--dom:swap" data-template-id="widget-b-template"
commandfor="#content-area" data-replace-strategy="outerHTML">
Load Widget B (Replace Container)
</button>
<button command="--fetch:get" data-url="/api/sidebar"
commandfor="#content-area" data-replace-strategy="beforeend">
Add Sidebar
</button>
<button command="--fetch:get" data-url="/api/header"
commandfor="#content-area" data-replace-strategy="afterbegin">
Prepend Header
</button>
</div>'><!-- Templates defined in the same HTML document --><templateid="widget-a-template"><divclass="widget widget-a"><h3>Widget A</h3><p>This content comes from a local template.</p></div></template><templateid="widget-b-template"><divclass="widget widget-b"><h3>Widget B</h3><p>This replaces the entire container.</p></div></template><divid="content-area"><!-- Swap with local templates using different strategies --><buttoncommand="--dom:swap" data-template-id="widget-a-template"
commandfor="#content-area" data-replace-strategy="innerHTML">
Load Widget A (Inner)
</button><buttoncommand="--dom:swap" data-template-id="widget-b-template"
commandfor="#content-area" data-replace-strategy="outerHTML">
Load Widget B (Replace Container)
</button><!-- Fetch remote content with precise insertion control --><buttoncommand="--fetch:get" data-url="/api/sidebar"
commandfor="#content-area" data-replace-strategy="beforeend">
Add Sidebar
</button><buttoncommand="--fetch:get" data-url="/api/header"
commandfor="#content-area" data-replace-strategy="afterbegin">
Prepend Header
</button></div>
Key Differences:
Philosophy: HTMX extends HTML as a hypermedia control. Invokers extends HTML for rich, client-side UI interactions.
Network: HTMX is chatty by design. Invokers is silent unless you explicitly use --fetch.
State: With HTMX, UI state often lives on the server. With Invokers, UI state lives in the DOM.
Use Case: HTMX is excellent for server-rendered apps (Rails, Django, PHP). Invokers excels at enhancing static sites, design systems, and front-end frameworks.
Alpine puts JavaScript logic in your HTML; Invokers keeps it out.
Alpine.js gives you framework-like reactivity and state management by embedding JavaScript expressions in x- attributes. Invokers achieves similar results using a predefined set of commands, keeping your markup free of raw JavaScript and closer to standard HTML.
Use Case: Textarea Character Counter
Show a live character count as a user types in a textarea.
Alpine.js: State and Logic in x-data
Alpine creates a small, self-contained component with its own state (message) and uses JS properties (message.length) directly in the markup.
<div x-data="{ message: '', limit: 140 }">
<textarea x-model="message" :maxlength="limit" class="input"></textarea>
<p class="char-count">
<span x-text="message.length">0</span> / <span x-text="limit">140</span>
</p>
</div>"><!-- Alpine puts a "sprinkle" of JavaScript directly in the HTML --><divx-data="{ message: '', limit: 140 }"><textareax-model="message" :maxlength="limit" class="input"></textarea><pclass="char-count"><spanx-text="message.length">0</span> / <spanx-text="limit">140</span></p></div>
Invokers: Declarative Commands and Expressions
Invokers uses the command-on attribute to listen for the input event and the {{...}} expression engine to update the target's text content. It describes the relationship between elements, not component logic.
<div>
<textarea id="message-input" maxlength="140" class="input"
command-on="input"
command="--text:set:{{this.value.length}}"
commandfor="char-count"></textarea>
<p class="char-count">
<span id="char-count">0</span> / 140
</p>
</div>'><!-- Invokers describes the event and action, no JS logic in the HTML --><div><textareaid="message-input" maxlength="140" class="input"
command-on="input"
command="--text:set:{{this.value.length}}"
commandfor="char-count"></textarea><pclass="char-count"><spanid="char-count">0</span> / 140
</p></div>
State: Alpine encourages creating explicit state (x-data). Invokers derives state directly from the DOM (e.g., this.value.length).
Paradigm: Alpine creates "mini-apps" in your DOM. Invokers creates declarative "event-action" bindings between elements.
Future: The command attribute is on a standards track. Alpine's syntax is specific to the library.
Stimulus organizes your JavaScript; Invokers helps you eliminate it.
Stimulus is a modest JavaScript framework that connects HTML to JavaScript objects (controllers). It’s designed for applications with significant custom JavaScript logic. Invokers is designed to handle common UI patterns with no custom JavaScript at all.
Use Case: Copy to Clipboard with Feedback
A user clicks a button to copy a URL to their clipboard, and the button provides feedback by changing its text to "Copied!" for a moment.
Stimulus: HTML Connected to a JS Controller
Stimulus requires a JavaScript controller to hold the logic for interacting with the clipboard API and managing the button's state (text change and timeout). The HTML contains data-* attributes to connect elements to this controller.
<div data-controller="clipboard">
<input data-clipboard-target="source" type="text"
value="https://example.com" readonly>
<button data-action="clipboard#copy" class="btn">
Copy Link
</button>
</div>'><!-- Stimulus connects HTML elements to a required JS controller --><divdata-controller="clipboard"><inputdata-clipboard-target="source" type="text"
value="https://example.com" readonly><buttondata-action="clipboard#copy" class="btn">
Copy Link
</button></div>
// A "clipboard_controller.js" file is requiredimport{Controller}from"@hotwired/stimulus"exportdefaultclassextendsController{statictargets=["source"]copy(event){// Logic to interact with the browser APInavigator.clipboard.writeText(this.sourceTarget.value)// Custom logic for UI feedbackconstoriginalText=event.currentTarget.textContentevent.currentTarget.textContent="Copied!"setTimeout(()=>{event.currentTarget.textContent=originalText},2000)}}
Invokers: Declarative Behavior with Command Chaining
Invokers has a built-in --clipboard:copy command. The UI feedback is handled declaratively by chaining commands in the data-and-then attribute. The entire workflow is defined in a single, readable line with no separate JavaScript file needed.
<div>
<input id="share-url" type="text" value="https://example.com" readonly>
<button type="button" class="btn"
command="--clipboard:copy"
commandfor="share-url"
data-and-then="--text:set:Copied!, --command:delay:2000, --text:set:Copy Link">
Copy Link
</button>
</div>'><!-- Invokers handles this with a single line of chained commands --><div><inputid="share-url" type="text" value="https://example.com" readonly><buttontype="button" class="btn"
command="--clipboard:copy"
commandfor="share-url"
data-and-then="--text:set:Copied!, --command:delay:2000, --text:set:Copy Link">
Copy Link
</button></div>
(Note: For more robust error handling, you could use data-after-success instead of data-and-then to ensure the feedback only runs if the copy action succeeds.)
Key Differences:
Ceremony: Stimulus requires a specific file structure and JS classes for every distinct piece of functionality. Invokers requires only HTML attributes for most tasks.
Source of Truth: In Stimulus, the behavior logic lives in the JS controller. In Invokers, the entire workflow is declared directly in the HTML.
Goal: Stimulus aims to give structure to complex applications that will inevitably have a lot of custom JS. Invokers aims to prevent you from needing to write JS in the first place for common UI patterns.
When to Choose: Use Stimulus when you have complex, stateful client-side logic that needs organization. Use Invokers when you want to build interactive UIs quickly with minimal or no JavaScript boilerplate.
Write interactive UIs without JavaScript. Invokers transforms static HTML into dynamic, interactive interfaces using declarative attributes. Perfect for progressive enhancement, component libraries, and reducing JavaScript complexity.
<button command="--toggle" commandfor="menu">Menu</button>
<nav id="menu" hidden>...</nav>
<form command-on="submit.prevent" command="--fetch:send" commandfor="#result">
<input name="query" placeholder="Search...">
<button type="submit">Search</button>
</form>
<div id="result"></div>'><!-- Toggle a menu --><buttoncommand="--toggle" commandfor="menu">Menu</button><navid="menu" hidden>...</nav><!-- Form with dynamic feedback --><formcommand-on="submit.prevent" command="--fetch:send" commandfor="#result"><inputname="query" placeholder="Search..."><buttontype="submit">Search</button></form><divid="result"></div>
Choose exactly what you need. Invokers now features a hyper-modular architecture with four tiers:
🏗️ Tier 0: Core polyfill (25.8 kB) - Standards-compliant foundation
These commands are built into modern browsers and work without any JavaScript framework. Invokers provides full polyfill support for cross-browser compatibility.
Input/Textarea Integration: URL commands like params-set, hash-set, and pathname-set can get their values from <input> or <textarea> elements by using commandfor to target the input element. If no value is provided in the command string, the value will be taken from the target element's value property.
Now add interactivity with text, classes, and attributes.
Tab Panels (Accessible by Default)
<divrole="tablist"><buttontype="button" command="--show" commandfor="panel-1" aria-expanded="true">
Home
</button><buttontype="button" command="--show" commandfor="panel-2" aria-expanded="false">
About
</button></div><div><divid="panel-1" role="tabpanel">Welcome to our homepage!</div><divid="panel-2" role="tabpanel" hidden>Learn more about us.</div></div>
<button type="button"
command="--text:set:✅ Saved!"
commandfor="status"
data-and-then="--class:add:success">
Save Document
</button>
<div id="status" class="status-message">Ready to save</div>
<button type="button"
command="--class:toggle:dark-theme"
commandfor="body"
data-and-then="--text:set:Theme toggled!">
🌙 Toggle Theme
</button>
<button type="button"
command="--animate:bounce"
data-animate-duration="1s"
data-animate-delay="200ms"
data-animate-iterations="2">
Celebrate!
</button>
<button type="button"
command="--storage:local:set:theme:dark"
data-storage-expires="3600"
data-and-then="--text:set:Preference saved!">
Save Dark Theme
</button>
<button type="button" command="--a11y:focus" commandfor="search-input">Focus Search</button>
<input type="text" id="search-input" placeholder="Search...">
<button type="button" command="--url:params:set:page:2">Go to Page 2</button>
<button type="button" command="--url:hash:set:section-about">Jump to About</button>'><!-- Change text and add visual feedback --><buttontype="button"
command="--text:set:✅ Saved!"
commandfor="status"
data-and-then="--class:add:success">
Save Document
</button><divid="status" class="status-message">Ready to save</div><!-- Toggle dark mode --><buttontype="button"
command="--class:toggle:dark-theme"
commandfor="body"
data-and-then="--text:set:Theme toggled!">
🌙 Toggle Theme
</button><!-- Animate elements with custom timing --><buttontype="button"
command="--animate:bounce"
data-animate-duration="1s"
data-animate-delay="200ms"
data-animate-iterations="2">
Celebrate!
</button><!-- Store user preferences --><buttontype="button"
command="--storage:local:set:theme:dark"
data-storage-expires="3600"
data-and-then="--text:set:Preference saved!">
Save Dark Theme
</button><!-- Focus management --><buttontype="button" command="--a11y:focus" commandfor="search-input">Focus Search</button><inputtype="text" id="search-input" placeholder="Search..."><!-- URL manipulation --><buttontype="button" command="--url:params:set:page:2">Go to Page 2</button><buttontype="button" command="--url:hash:set:section-about">Jump to About</button>
<button type="button"
command="--fetch:get"
data-url="/api/latest-posts"
commandfor="posts-container"
data-loading-template="spinner">
Load Latest Posts
</button>
<div id="posts-container">Posts will appear here...</div>
<template id="spinner">
<p>Loading posts...</p>
</template>'><!-- Fetch and display server content --><buttontype="button"
command="--fetch:get"
data-url="/api/latest-posts"
commandfor="posts-container"
data-loading-template="spinner">
Load Latest Posts
</button><divid="posts-container">Posts will appear here...</div><templateid="spinner"><p>Loading posts...</p></template>
<form id="contact-form" action="/api/contact" method="post">
<input type="text" name="name" required>
<button type="button"
command="--fetch:send"
commandfor="contact-form"
data-response-target="#result-area"
data-loading-template="spinner">
Submit Form
</button>
</form>
<div id="result-area">Response will appear here...</div>
<template id="spinner">
<div>Loading...</div>
</template>'><!-- Submit form data and update content dynamically --><formid="contact-form" action="/api/contact" method="post"><inputtype="text" name="name" required><buttontype="button"
command="--fetch:send"
commandfor="contact-form"
data-response-target="#result-area"
data-loading-template="spinner">
Submit Form
</button></form><divid="result-area">Response will appear here...</div><templateid="spinner"><div>Loading...</div></template>
The data-response-target attribute specifies where to display the server response, while data-loading-template shows a loading indicator during submission.
📚 Level 3: Advanced Workflows
Chain multiple commands together for complex interactions.
<button type="button"
command="--text:set:Processing complete!"
commandfor="status"
data-and-then="--class:add:success">
Complete Process
</button>
<div id="status">Ready to process</div>'><!-- Chain two commands: change text, then add a class --><buttontype="button"
command="--text:set:Processing complete!"
commandfor="status"
data-and-then="--class:add:success">
Complete Process
</button><divid="status">Ready to process</div>
<button type="button"
command="--fetch:get"
data-url="/api/user-data"
commandfor="profile"
data-after-success="--class:add:loaded,--text:set:Profile loaded!"
data-after-error="--class:add:error,--text:set:Failed to load profile"
data-after-complete="--attr:set:aria-busy:false">
Load Profile
</button>
<div id="profile" aria-busy="false">Profile will load here...</div>'><!-- Different commands based on success/failure --><buttontype="button"
command="--fetch:get"
data-url="/api/user-data"
commandfor="profile"
data-after-success="--class:add:loaded,--text:set:Profile loaded!"
data-after-error="--class:add:error,--text:set:Failed to load profile"
data-after-complete="--attr:set:aria-busy:false">
Load Profile
</button><divid="profile" aria-busy="false">Profile will load here...</div>
Invokers features a powerful plugin system that allows you to extend functionality through middleware hooks. Plugins can intercept command execution at various lifecycle points, enabling features like analytics, security checks, UI enhancements, and more.
Plugins are objects that implement the InvokerPlugin interface and can register middleware functions for specific hook points in the command execution lifecycle:
You can also register middleware globally without creating a full plugin:
// Register global middlewarewindow.Invoker.instance.registerMiddleware(window.Invoker.HookPoint.BEFORE_COMMAND,(context)=>{console.log('All commands pass through here:',context.fullCommand);});// Unregister specific middlewarewindow.Invoker.instance.unregisterMiddleware(window.Invoker.HookPoint.BEFORE_COMMAND,middlewareFunction);
Plugins can implement onRegister and onUnregister methods for setup and cleanup:
constmyPlugin={name: 'my-plugin',onRegister(manager){console.log('Plugin registered, setting up...');// Initialize plugin resources},onUnregister(manager){console.log('Plugin unregistered, cleaning up...');// Clean up resources}};// Register the pluginwindow.Invoker.instance.registerPlugin(myPlugin);
Invokers includes a comprehensive set of extended commands that are automatically available when you import the library. These provide advanced features for real-world applications:
Automatically Included Commands:
Server Communication: --fetch:get, --fetch:send - Load and send data to servers
Media Controls: --media:toggle, --media:seek, --media:mute - Full media player controls
For applications that only need specific commands, you can selectively register them:
import{registerAll}from'https://esm.sh/invokers/commands';// Register only media and fetch commandsregisterAll(['--media:toggle','--media:seek','--fetch:get']);
This can help reduce bundle size in applications with strict performance requirements.
📚 Detailed Command Reference
The --storage commands provide comprehensive localStorage and sessionStorage management with advanced features like JSON support, expiration, and metadata.
<button type="button" command="--storage:local:set:username:john">Save Username</button>
<button type="button" command="--storage:session:set:temp-data:123">Save Temp Data</button>
<button type="button" command="--storage:local:get:username" commandfor="username-display">Load Username</button>
<div id="username-display">Username will appear here</div>
<button type="button" command="--storage:local:remove:username">Clear Username</button>
<button type="button" command="--storage:local:clear">Clear All Local Data</button>'><!-- Store simple values --><buttontype="button" command="--storage:local:set:username:john">Save Username</button><buttontype="button" command="--storage:session:set:temp-data:123">Save Temp Data</button><!-- Retrieve values --><buttontype="button" command="--storage:local:get:username" commandfor="username-display">Load Username</button><divid="username-display">Username will appear here</div><!-- Remove specific keys --><buttontype="button" command="--storage:local:remove:username">Clear Username</button><!-- Clear all storage --><buttontype="button" command="--storage:local:clear">Clear All Local Data</button>
<button type="button"
command="--storage:local:set:user-settings"
data-storage-json="true">
Save Settings Object
</button>
<button type="button"
command="--storage:local:set:session-token:abc123"
data-storage-expires="3600">
Save Token (expires in 1 hour)
</button>
<button type="button" command="--storage:local:has:username" commandfor="status">Check Username</button>
<button type="button" command="--storage:local:size" commandfor="size-display">Show Storage Size</button>
<button type="button" command="--storage:local:keys" commandfor="keys-list">List All Keys</button>'><!-- JSON storage with automatic serialization --><buttontype="button"
command="--storage:local:set:user-settings"
data-storage-json="true">
Save Settings Object
</button><!-- Expiring data (in seconds) --><buttontype="button"
command="--storage:local:set:session-token:abc123"
data-storage-expires="3600">
Save Token (expires in 1 hour)
</button><!-- Check if key exists --><buttontype="button" command="--storage:local:has:username" commandfor="status">Check Username</button><!-- Get storage size --><buttontype="button" command="--storage:local:size" commandfor="size-display">Show Storage Size</button><!-- List all keys --><buttontype="button" command="--storage:local:keys" commandfor="keys-list">List All Keys</button>
// Programmatic storage accessconstsettings={theme: 'dark',lang: 'en'};localStorage.setItem('user-prefs',JSON.stringify(settings));// Then retrieve in HTML<buttoncommand="--storage:local:get:user-prefs"commandfor="prefs-display">Load Preferences</button>
The --animate command provides CSS-based animations with customizable timing and effects.
For too long, creating interactive UIs has meant a disconnect between structure (HTML) and behavior (JavaScript). This leads to scattered code, accessibility oversights, and boilerplate that slows down development. The web platform is evolving to fix this.
Invokers embraces this evolution, letting you build complex interactions with the simplicity and elegance of plain HTML.
Before: The Manual JavaScript Way
<button id="menu-toggle">Menu</button>
<nav id="main-menu" hidden>...</nav>
<script>
// Somewhere else in your project...
document.getElementById('menu-toggle').addEventListener('click', (e) => {
const menu = document.getElementById('main-menu');
menu.hidden = !menu.hidden;
// We have to remember to manually sync accessibility state.
e.target.setAttribute('aria-expanded', !menu.hidden);
});
</script>"><!-- The button is just a button. Its purpose is hidden in a script file. --><buttonid="menu-toggle">Menu</button><navid="main-menu" hidden>...</nav><script>// Somewhere else in your project...document.getElementById('menu-toggle').addEventListener('click',(e)=>{constmenu=document.getElementById('main-menu');menu.hidden=!menu.hidden;// We have to remember to manually sync accessibility state.e.target.setAttribute('aria-expanded',!menu.hidden);});</script>
After: The Invokers Way (The Future Standard)
With Invokers, your HTML becomes the single source of truth. It's clean, readable, and requires no custom JavaScript for this common pattern.
<button type="button" command="--toggle" commandfor="main-menu" aria-expanded="false">
Menu
</button>
<nav id="main-menu" hidden>...</nav>
<script type="module" src="https://esm.sh/invokers"></script>"><!-- The button's purpose is clear just by reading the markup. --><buttontype="button" command="--toggle" commandfor="main-menu" aria-expanded="false">
Menu
</button><navid="main-menu" hidden>...</nav><!-- Add Invokers to your page, and you're done. --><scripttype="module" src="https://esm.sh/invokers"></script>
⚡ Advanced Events (Opt-In)
Invokers includes powerful advanced event features that transform it from a click-driven library into a fully reactive framework. These features are opt-in to keep the core library lightweight.
To use advanced event features, import and call enableAdvancedEvents() once in your application:
// In your main application script (e.g., app.js)import'invokers';// Load the core library firstimport{enableAdvancedEvents}from'invokers/advanced';// Call this function once to activate all new event featuresenableAdvancedEvents();
New Attributes: command-on and data-on-event
Once enabled, you gain access to two new declarative attributes for triggering commands from any DOM event.
command-on: Trigger Commands from Any Event
Allows any element to execute a command in response to any DOM event, not just button clicks.
Allows elements to listen for custom events dispatched from anywhere on the page.
<button command="--emit:notify:{\"message\":\"Profile Saved!\",\"type\":\"success\"}">
Save Profile
</button>
<div id="toast-notification"
data-on-event="notify"
command="--show">
Notification will appear here!
</div>'><!-- Button emits a custom event --><buttoncommand="--emit:notify:{\"message\":\"Profile Saved!\",\"type\":\"success\"}">
Save Profile
</button><!-- Separate toast listens for it --><divid="toast-notification"
data-on-event="notify"
command="--show">
Notification will appear here!
</div>
Dynamic Data with {{...}} Syntax
Inject dynamic data from events directly into command attributes using {{...}} placeholders. Supports full JavaScript-like expressions for complex data manipulation.
Function calls: obj.method() (methods are accessible but not callable)
Object/array creation: {}, [], new
Global access: window, document, console
Loops, assignments, or any imperative code
Template literals or other ES6+ features
Performance & Caching:
Expressions are automatically cached for optimal performance. Parsed expressions are stored in an LRU cache, making repeated evaluations of the same expression extremely fast.
Error Handling:
Invalid expressions return undefined and log helpful error messages to the console. Your UI gracefully degrades when expressions fail.
<button command="--set:{{ nonexistent.property }}">Click me</button>'><!-- Safe fallback: if expression fails, attribute uses empty value --><buttoncommand="--set:{{ nonexistent.property }}">Click me</button>
Enhance event handling with modifiers:
Modifier
Description
Example
.prevent
Calls event.preventDefault()
command-on="submit.prevent"
.stop
Calls event.stopPropagation()
command-on="click.stop"
.once
Listener removes itself after one trigger
command-on="mouseenter.once"
.window
Attaches to global window object
command-on="keydown.window.ctrl.s"
.debounce
Waits for pause in events (250ms default)
command-on="input.debounce"
.debounce.<ms>
Custom debounce delay
command-on="input.debounce.300"
.{key}
Only triggers on specific key
command-on="keydown.enter.prevent"
Command Chaining with Expressions
Combine expressions with command chaining for dynamic, data-driven workflows:
Templates automatically rewrite @closest selectors to use generated unique IDs, enabling proper scoping:
'><templateid="item-template"><divclass="item" id="item-{{__uid}}"><spandata-tpl-text="name"></span><buttoncommand="--class:toggle:active" commandfor="@closest(.item)">
Toggle
</button></div></template><!-- The @closest selector becomes #item-123 when rendered -->
Complete Example: Todo List
<template id="todo-item-template">
<div class="todo-item" id="todo-{{__uid}}">
<input type="checkbox"
command="--class:toggle:completed"
commandfor="@closest(.todo-item)">
<span data-tpl-text="text"></span>
<button command="--dom:remove" commandfor="@closest(.todo-item)">
Delete
</button>
</div>
</template>
<form command-on="submit.prevent"
command="--dom:append"
commandfor="todo-list"
data-template-id="todo-item-template"
data-with-json='{"text": ""}'>
<input name="todo-text" placeholder="Add a todo..." required>
<button type="submit">Add</button>
</form>
<ul id="todo-list" class="todo-list"></ul>"><!-- Template for todo items --><templateid="todo-item-template"><divclass="todo-item" id="todo-{{__uid}}"><inputtype="checkbox"
command="--class:toggle:completed"
commandfor="@closest(.todo-item)"><spandata-tpl-text="text"></span><buttoncommand="--dom:remove" commandfor="@closest(.todo-item)">
Delete
</button></div></template><!-- Form to add new todos --><formcommand-on="submit.prevent"
command="--dom:append"
commandfor="todo-list"
data-template-id="todo-item-template"
data-with-json='{"text": ""}'><inputname="todo-text" placeholder="Add a todo..." required><buttontype="submit">Add</button></form><!-- Container for todo items --><ulid="todo-list" class="todo-list"></ul>
This creates a fully functional todo list where:
New items are added via the form
Checkboxes toggle completion state
Delete buttons remove items
Each item has a unique ID for proper scoping
🎯 Advanced commandfor Selectors
Invokers supports powerful contextual selectors that go beyond simple IDs, enabling complex DOM targeting patterns without JavaScript.
Selector
Description
Example
@closest(selector)
Target the closest ancestor matching the selector
commandfor="@closest(.card)"
@child(selector)
Target direct children matching the selector
commandfor="@child(.item)"
@children(selector)
Target all children matching the selector
commandfor="@children(.item)"
You can also use any standard CSS selector directly:
<button command="--hide" commandfor=".modal">Close All Modals</button>
<button command="--toggle" commandfor="#sidebar .menu-item">Toggle Menu Items</button>
<button command="--class:add:active" commandfor="article[data-category='featured']">
Mark Featured Articles
</button>"><!-- Target all elements with a class --><buttoncommand="--hide" commandfor=".modal">Close All Modals</button><!-- Target elements within a specific container --><buttoncommand="--toggle" commandfor="#sidebar .menu-item">Toggle Menu Items</button><!-- Complex selectors work too --><buttoncommand="--class:add:active" commandfor="article[data-category='featured']">
Mark Featured Articles
</button>
// Start minimal (25.8 kB)importinvokersfrom'invokers';// Add what you needimport{registerBaseCommands}from'invokers/commands/base';import{registerFormCommands}from'invokers/commands/form';registerBaseCommands(invokers);registerFormCommands(invokers);// ~60 kB total - only what you use
After (v1.5.x) - Compatibility Layer:
// For existing apps that need all commands (82 kB)import'invokers/compatible';// All commands are now available - no changes needed to your HTML
For existing applications that want to upgrade to v1.5.x without changing their code, use the compatibility layer:
// Drop-in replacement for the old monolithic importimport{InvokerManager}from'invokers/compatible';// OR for side-effects onlyimport'invokers/compatible';
Enable advanced features: For dynamic applications
Base Commands: Essential UI state management
Form Commands: Content and form manipulation
DOM Commands: Dynamic content insertion
Flow Commands: Async operations and navigation
Media Commands: Rich media controls
Browser Commands: Browser API integration
Data Commands: Complex data operations
Event Triggers: command-on attribute for any DOM event
Expression Engine: {{expression}} syntax for dynamic parameters
Command Chaining: data-and-then for complex workflows
Interest Invokers: interestfor for hover interactions
// Core APIinvokers.register(name,callback);invokers.executeCommand(command,targetId,source);// Global API (when using CDN)window.Invoker.register(name,callback);window.Invoker.executeCommand(command,targetId,source);
Core only: 25.8 kB (polyfill + engine)
Essential UI: ~60 kB (core + base + form)
Full power: ~200 kB (all packs + advanced)
Original v1.4: 160 kB (everything forced)
Start with core, add incrementally
Use tree-shaking with ES modules
Enable advanced features only when needed
Leverage browser caching for separate chunks
Node.js 16+
TypeScript 5.7+
Pridepack (build tool)
npm run build # Build all modules
npm run test# Run tests
npm run dev # Development mode
npm run clean # Clean build artifacts