Show HN: Invokers: A Polyfill and Extension for HTML Invoker Commands

Show HN: Invokers: A Polyfill and Extension for HTML Invoker Commands

npm version

License: MIT

✨ Invokers

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.

? Table of Contents

Features

  • 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.
  • ? Singleton Architecture: Optimized internal architecture ensures consistent behavior and prevents duplicate registrations.

Quick Demo

See Invokers in action with this copy-paste example:

<!DOCTYPE html>
<html>
<head>
  <!-- Add Invokers via CDN (includes all commands) -->
  <script type="module" src="https://esm.sh/invokers/compatible"></script>
</head>
<body>
  <!-- Toggle a navigation menu with zero JavaScript -->
  <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>
    <!-- Dismiss button that hides itself -->
    <button type="button" command="--hide" commandfor="nav-menu"></button>
  </nav>

  <!-- Hover cards work automatically with Interest Invokers -->
  <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>

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.

Core Proposal Features

  • 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

Example from the Specification

<button command="show-modal" commandfor="my-dialog">Open Dialog</button>
<dialog id="my-dialog">Hello World</dialog>

How Invokers Extends This

Invokers provides a complete polyfill for the Invoker Commands API while adding extensive enhancements:

  • Extended Command Set: Adds 50+ custom commands (--toggle, --fetch:get, --media:play, etc.) beyond the spec's basic commands
  • 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.

Core Proposal Features

  • 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

Example from the Specification

<a href="/profile" interestfor="user-card">@username</a>
<div id="user-card" popover="hint">User details...</div>

How Invokers Extends This

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

Popover API Integration

Invokers has deep integration with the Popover API, automatically handling popover lifecycle and accessibility when using popover attributes.

Automatic Behaviors

  • 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

Current Browser Support

  • Chrome/Edge: Full Invoker Commands support (v120+)
  • Firefox: Partial support, actively developing
  • Safari: Under consideration
  • Polyfill Coverage: Invokers provides complete fallback for all browsers

Standards Timeline

  • Invoker Commands: Graduated from OpenUI, in WHATWG HTML specification
  • Interest Invokers: Active proposal, expected to graduate soon
  • Popover API: Already shipping in major browsers

Migration Path

As browsers implement these features natively:

  1. Invokers will automatically detect native support
  2. Polyfill behaviors will gracefully disable
  3. Your HTML markup remains unchanged
  4. 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.

? How Does This Compare?

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.

Feature Vanilla JS HTMX Alpine.js Stimulus Invokers
Philosophy Imperative Hypermedia (Server-centric) JS in HTML (Component-like) JS Organization (MVC-like) Declarative HTML (Browser-centric)
Standards-Aligned (Core Mission)
Primary Use Case Anything Server-rendered partials Self-contained UI components Organizing complex JS JS-free UI patterns & progressive enhancement
JS Required for UI Always For server comms For component logic Always (in controllers) Often none for common patterns
Accessibility Manual Manual Manual Manual (Automatic ARIA management)
Learning Curve High Medium (Hypermedia concepts) Low (Vue-like syntax) Medium (Controller concepts) Very Low (HTML attributes)

? vs HTMX

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.

Use Case: Inline Editing

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.

<!-- HTMX requires a server to serve the edit-form fragment -->
<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>

<!-- 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.

<!-- Invokers handles this entirely on the client, no server needed -->
<div class="user-profile">
  <!-- 1. The view state (visible by default) -->
  <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>

  <!-- 2. The edit state (hidden by default) -->
  <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>

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.

<!-- HTMX requires server endpoints for each content type -->
<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>

<!-- 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.

Widget A

This content comes from a local template.

">
<!-- Templates defined in the same HTML document -->
<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">
  <!-- Swap with local templates using different strategies -->
  <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>

  <!-- Fetch remote content with precise insertion control -->
  <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>

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.

? vs Alpine.js

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.

<!-- Alpine puts a "sprinkle" of JavaScript directly in the HTML -->
<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>

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.

<!-- Invokers describes the event and action, no JS logic in the HTML -->
<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>

Key Differences:

  • Syntax: Alpine uses custom JS-like attributes (x-data, x-text). Invokers uses standard-proposal attributes (command, commandfor) and CSS-like command names (--text:set).
  • 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.

? vs Stimulus

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.

<!-- Stimulus connects HTML elements to a required JS 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>
// A "clipboard_controller.js" file is required
import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["source"]

  copy(event) {
    // Logic to interact with the browser API
    navigator.clipboard.writeText(this.sourceTarget.value)

    // Custom logic for UI feedback
    const originalText = event.currentTarget.textContent
    event.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.

<!-- Invokers handles this with a single line of chained commands -->
<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>

(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.

? Why Invokers?

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.

<!-- Toggle a menu -->
<button command="--toggle" commandfor="menu">Menu</button>
<nav id="menu" hidden>...</nav>

<!-- Form with dynamic feedback -->
<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>

? Modular Architecture

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
  • ⚡ Tier 1: Essential commands (~30 kB) - Basic UI interactions
  • ? Tier 2: Specialized packs (25-47 kB each) - Advanced functionality
  • ? Tier 3: Reactive engine (26-42 kB) - Dynamic templating & events

? Installation & Basic Usage

Core Installation (25.8 kB)

For developers who want just the standards polyfill:

import 'invokers';
// That's it! Now command/commandfor attributes work
<!-- Native/polyfilled commands work immediately -->
<button command="toggle-popover" commandfor="menu">Menu</button>
<div id="menu" popover>Menu content</div>

Essential UI Commands (+30 kB)

Add the most common interactive commands:

import invokers from 'invokers';
import { registerBaseCommands } from 'invokers/commands/base';
import { registerFormCommands } from 'invokers/commands/form';

registerBaseCommands(invokers);
registerFormCommands(invokers);
<!-- Now you can use essential commands -->
<button command="--toggle" commandfor="sidebar">Toggle Sidebar</button>
<button command="--class:toggle:dark-mode" commandfor="body">Dark Mode</button>
<button command="--text:set:Hello World!" commandfor="output">Set Text</button>

?️ Command Packs

Tier 1: Essential Commands

Base Commands (invokers/commands/base) - 29.2 kB

Essential UI state management without DOM manipulation.

import { registerBaseCommands } from 'invokers/commands/base';
registerBaseCommands(invokers);

Commands: --toggle, --show, --hide, --class:*, --attr:*

<button command="--toggle" commandfor="menu">Menu</button>
<button command="--class:add:active" commandfor="tab1">Activate Tab</button>
<button command="--attr:set:aria-expanded:true" commandfor="dropdown">Expand</button>

Form Commands (invokers/commands/form) - 30.5 kB

Form interactions and content manipulation.

import { registerFormCommands } from 'invokers/commands/form';
registerFormCommands(invokers);

Commands: --text:*, --value:*, --focus, --disabled:*, --form:*, --input:step

<button command="--text:set:Form submitted!" commandfor="status">Submit</button>
<button command="--value:set:admin@example.com" commandfor="email">Use Admin Email</button>
<button command="--input:step:5" commandfor="quantity">+5</button>

Tier 2: Specialized Commands

DOM Manipulation (invokers/commands/dom) - 47.1 kB

Dynamic content insertion and templating.

import { registerDomCommands } from 'invokers/commands/dom';
registerDomCommands(invokers);

Commands: --dom:*, --template:*

<button command="--dom:append" commandfor="list" data-template-id="item-tpl">Add Item</button>
<button command="--template:render:user-card" commandfor="output" 
        data-name="John" data-email="john@example.com">Render User</button>

Flow Control (invokers/commands/flow) - 45.3 kB

Async operations, navigation, and data binding.

import { registerFlowCommands } from 'invokers/commands/flow';
registerFlowCommands(invokers);

Commands: --fetch:*, --navigate:*, --emit:*, --command:*, --bind:*

<!-- Basic fetch with replace strategies -->
<button command="--fetch:get" data-url="/api/users" commandfor="user-list"
        data-replace-strategy="innerHTML">Load Users</button>

<!-- Form submission with custom replace strategy -->
<form id="contact-form" action="/api/contact" method="post"></form>
<button command="--fetch:send" commandfor="contact-form"
        data-response-target="#response"
        data-replace-strategy="outerHTML">Send Message</button>

<button command="--navigate:to:/dashboard">Go to Dashboard</button>
<input command-on="input" command="--bind:value" data-bind-to="#output" data-bind-as="text">

Replace Strategies:

  • innerHTML (default): Replace target element's content
  • outerHTML: Replace entire target element
  • beforebegin/afterbegin/beforeend/afterend: Insert adjacent to target

Media & Animation (invokers/commands/media) - 27.7 kB

Rich media controls and interactions.

Stay Informed

Get the best articles every day for FREE. Cancel anytime.