Documentation

Reference for talkDOM 0.5.0 · Released September 20, 2026.

Installation

Choose whichever method fits your project.

CDN

Add a single script tag. No build tools, no configuration.

<script src="https://cdn.jsdelivr.net/npm/talkdom@0.5.0/dist/talkdom.min.js"></script>

npm

npm install talkdom@0.5.0

The package contains classic browser scripts that expose window.talkDOM. It does not export a named ES module or Node API. Node 24.15+ is needed for development tooling, not browser usage.

That's it. talkDOM is ready to use.

Core Concepts

talkDOM uses two HTML attributes to wire up interactions: receiver and sender.

Receivers

A receiver attribute marks an element as a message target. The value is the receiver's name.

<div receiver="content"></div>

Senders

A sender attribute makes an element dispatch messages on click. Messages follow Smalltalk keyword syntax:

receiver keyword: arg keyword: arg

The first word is the receiver name, followed by keyword-argument pairs.

Example

Click the button to fetch /page.html and insert its content into the receiver:

<div receiver="content"></div>
<button sender="content get: /page.html apply: inner">Load</button>

Names are literal and whitespace-aware. Extra names before the first keyword are aliases; method arguments are not receiver names. talkDOM.receivers(name) returns current matches, including same-turn DOM changes.

Apply Operations

The apply: keyword determines how fetched content is inserted into the receiver. Four modes are available:

Mode Behavior
inner Replaces innerHTML
text Sets textContent
append Appends via insertAdjacentHTML
outer Replaces the receiver element itself via outerHTML

Restricting operations

Use the accepts attribute to restrict which operations a receiver allows:

<div receiver="output" accepts="text inner"></div>

This receiver will reject append and outer operations.

Use notice text: Saved for a literal text update. accepts uses whitespace-separated operation tokens; missing/empty values allow all supported operations. Invalid/denied applies emit error events. Set talkDOM.config.strictApply = true to reject them and stop pipes; legacy promises resolve by default.

HTTP Methods

talkDOM supports four HTTP keywords. Each takes a URL and returns the response text:

get:    → GET request
post:   → POST request
put:    → PUT request
delete: → DELETE request

Combined fetch + apply

Combine an HTTP method with apply: in a single message to fetch and insert in one step:

<!-- GET and replace inner HTML -->
<button sender="feed get: /api/posts apply: inner">Refresh</button>

<!-- POST and append the response -->
<button sender="feed post: /api/posts apply: append">Add</button>

<!-- DELETE and replace outer HTML (removes element) -->
<button sender="item delete: /api/posts/1 apply: outer">Remove</button>

Message Composition

Messages can be composed using pipes and semicolons for complex workflows.

Pipes — sequential chaining

The | operator chains messages sequentially. The result of each step is passed to the next:

<button sender="content get: /search | content apply: inner">
  Search
</button>

Semicolons — parallel independent chains

The ; operator runs multiple independent message chains in parallel:

<button sender="sidebar get: /nav apply: inner; main get: /home apply: inner">
  Reset
</button>

Both fetches happen simultaneously. Neither depends on the other.

A successful pipe waits for every matching receiver and passes the last receiver’s result forward. Failures stop that pipe while other started work continues. Semicolons, pipes, and whitespace tokens ending in a colon are reserved; quotes/backslashes do not escape them. Use structured deliver arguments for arbitrary data.

URL History

A sender with push-url updates the URL only after its full message succeeds. Cancellation or failure leaves the URL unchanged; already completed independent chains are not rolled back.

<a href="/about" sender="content get: /partials/about.html apply: inner" push-url="/about">About</a>

Back/Forward and reload restore receiver markup snapshots, including the initial view, without repeating POST, DELETE, or append commands. Empty push-url values use the first message argument. URLs must resolve to the page origin.

Restoration replaces receiver nodes. Delegate application listeners, keep receiver layout consistent across reloads, and do not rely on snapshots for unsaved form properties or JavaScript state.

Polling

Receivers can automatically refresh their content at a set interval using the poll: keyword in the receiver attribute:

<div receiver="status get: /api/status apply: inner poll: 5s"></div>

This fetches /api/status every 5 seconds and replaces the element's inner HTML with the response.

Polling stops automatically when the element is removed from the DOM. Elements added dynamically (e.g., via fragment injection) are picked up automatically by a MutationObserver — no manual setup needed.

Poller limit

A maximum of 64 concurrent pollers is enforced by default. Adjust via:

talkDOM.maxPollers = 128;

Identical declarations share a poller and use current group members. Edited attributes and outer replacements are reconciled. Ticks do not overlap while work is pending; errors are caught and emit lifecycle events before later ticks recover.

Persistence

Add the persist attribute to a receiver to save its content to localStorage:

<div receiver="preferences" persist></div>

Content is saved after successful apply operations and restored on startup. Outer swaps save the replacement markup. Storage failures warn without failing the DOM update; corrupt entries are ignored. Useful for user preferences, draft content, or UI state that should survive page refreshes.

Keys use talkDOM:<primary name>, shared across pages of the same origin. Storage is plain text; restored markup is trusted and does not recheck accepts. Do not persist secrets.

Server Triggers

Servers can fire client-side talkDOM messages by including the X-TalkDOM-Trigger response header:

X-TalkDOM-Trigger: notifications get: /api/alerts apply: inner

When talkDOM receives a response with this header, it executes the message as if a sender had been clicked.

CORS

For cross-origin requests, the server must expose the header:

Access-Control-Expose-Headers: X-TalkDOM-Trigger

Request Headers

Core requests include X-TalkDOM-Request and the primary receiver name. Sensitive current-page and CSRF headers go only to same-origin or explicitly trusted destinations:

Header Description
X-TalkDOM-Request Always present. Identifies the request as coming from talkDOM.
X-TalkDOM-Current-URL The current page URL at the time of the request.
X-TalkDOM-Receiver The name of the receiver that will handle the response.

CSRF Protection

Same-origin or explicitly trusted POST, PUT, and DELETE requests automatically include an X-CSRF-Token header, read from a meta tag:

<meta name="csrf-token" content="your-token-here">

Set startup configuration before loading core, or edit talkDOM.config later. trustedOrigins uses exact origins, without wildcards:

window.talkDOMConfig = {
  trustedOrigins: ["https://api.example.com"],
  includeCurrentURL: false,
  allowServerTriggers: false,
  strictApply: true
};

Current-page headers include query/fragment when enabled. Readable server-trigger responses are trusted commands by default, including cross-origin responses; disable allowServerTriggers when that is unsuitable.

Custom Form Requests

Form body selectors are not built into 0.5.0. Register a custom method for payloads and use normal form validation before dispatch. This same-origin example sends FormData and applies a trusted response:

talkDOM.methods["submit:form:"] = async function(el, url, selector) {
  const form = document.querySelector(selector);
  if (!form || !form.reportValidity()) throw new Error("Invalid form");
  const target = new URL(url, document.baseURI);
  if (target.origin !== location.origin) throw new Error("Same-origin URL required");
  const token = document.querySelector('meta[name="csrf-token"]')?.content;
  const response = await fetch(target, {
    method: "POST", body: new FormData(form),
    headers: token ? {"X-CSRF-Token": token} : {}
  });
  if (!response.ok) throw new Error("HTTP " + response.status);
  return talkDOM.methods["apply:"](el, await response.text(), "inner");
};
// sender="result submit: /save form: #my-form"

Custom JSON Requests

Core methods do not serialize JSON. A custom method can accept a structured value through talkDOM.deliver(), avoiding message-string escaping:

talkDOM.methods["save-json:"] = async function(el, payload) {
  const token = document.querySelector('meta[name="csrf-token"]')?.content;
  const headers = {"Content-Type": "application/json"};
  if (token) headers["X-CSRF-Token"] = token;
  const response = await fetch("/api/data", {
    method: "POST", headers, body: JSON.stringify(payload)
  });
  if (!response.ok) throw new Error("HTTP " + response.status);
  return talkDOM.methods["apply:"](el, await response.text(), "inner");
};
await talkDOM.deliver(element, "save-json:", [{name: "example"}]);

Custom Loading State

Loading indicators are an application extension. This site registers the following example once in its shared script:

talkDOM.methods["get:apply:loading:"] = function(el, url, op, cls) {
  el.classList.add(cls);
  return talkDOM.methods["get:apply:"](el, url, op)
    .finally(function() { el.classList.remove(cls); });
};
<div receiver="content"></div>
<button sender="content get: /data apply: inner loading: is-loading">Load</button>

Define the is-loading class in your CSS. The built-in method list does not include loading:.

Lifecycle Events

talkDOM dispatches custom events on receiver elements after operations complete:

talkdom:done

Fired after a successful operation. The detail object includes:

{ receiver, selector, args, originalReceiver }

talkdom:error

Fired when an operation fails. The detail object includes:

{ receiver, selector, args, originalReceiver, error }

Example

document.querySelector('[receiver="content"]')
  .addEventListener('talkdom:done', (e) => {
    console.log('Updated:', e.detail.receiver);
  });

Programmatic API

Sending messages from JavaScript

Use talkDOM.send() to dispatch messages programmatically. Returns a Promise:

await talkDOM.send("content get: /api/data apply: inner");

Custom methods

Register custom keywords by adding functions to talkDOM.methods:

talkDOM.methods["highlight:"] = function(el, arg) {
  el.style.backgroundColor = arg;
};

// Now usable in sender attributes:
// sender="box highlight: yellow"

Separate sends remain concurrent and can complete out of order. Use structured per-element delivery for data containing message delimiters:

await talkDOM.deliver(element, "text:", ["Literal ; and |"]);
const elements = talkDOM.receivers("content");

WebSocket Plugin

The optional WebSocket plugin adds server-push via WebSocket as an alternative to polling. Load it after the core library:

<script src="https://cdn.jsdelivr.net/npm/talkdom@0.5.0/dist/talkdom.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/talkdom@0.5.0/dist/talkdom-ws.min.js"></script>

Receiving

Add ws: as the last keyword on a receiver with a WebSocket URL as its argument:

<div receiver="feed ws: ws://localhost:3000/updates"></div>

The server sends JSON messages to control what gets applied:

{"receiver": "feed", "content": "<p>New post</p>", "op": "append"}
Field Required Description
receiver no Optional name within this connection; omission broadcasts to its subscribers
content no HTML or text content
op no inner (default), text, append, outer

JSON targets only subscribers of the sending connection and preserves zero/false content. Raw command strings remain document-wide; trust the sending endpoint:

feed text: Updated!

Sending

The plugin registers a ws:send: method. The receiver element's value (for inputs) or text content is sent over the WebSocket connection:

<input receiver="chatbox ws: ws://localhost:3000/chat" type="text">
<button sender="chatbox ws:send: ws://localhost:3000/chat">Send</button>

Shared connections

Multiple receivers pointing to the same URL share a single WebSocket connection:

<div receiver="messages ws: ws://localhost:3000/live"></div>
<div receiver="presence ws: ws://localhost:3000/live"></div>

Reconnection

Connections automatically reconnect with exponential backoff (1s initial, 30s max, ±25% jitter). Automatic connections close when their final subscriber disappears. Manually owned connections remain alive and reconnect until explicitly disconnected.

WebSocket events

talkdom:ws:open

Fired when a WebSocket connection opens. Detail: { url }

talkdom:ws:close

Fired when a connection closes. Detail: { url, code, reason }

talkdom:ws:error

Fired on connection error. Detail: { url }

Programmatic WebSocket API

talkDOM.ws.connect("ws://localhost:3000/live");
// Wait until the connection is open before sending.
talkDOM.ws.send("ws://localhost:3000/live", { action: "subscribe" });
talkDOM.ws.disconnect("ws://localhost:3000/live");

talkDOM.ws.connections;      // { "ws://...": { state, receivers } }
talkDOM.ws.maxConnections;   // default 16

Security

talkDOM does not sanitize HTML. You must ensure that server responses are safe before inserting them into the DOM.

CSRF tokens are automatically read from a <meta name="csrf-token"> tag and sent only to same-origin or explicitly trusted destinations on non-GET requests.

Avoid storing sensitive data in receivers that use the persist attribute, as content is saved to localStorage.

Browser Support

Targets current browsers with native Fetch, Promise, MutationObserver, Map/Set, WeakMap/WeakSet, URL, and DOM Range support. Chromium integration is tested; historical minimum versions are not verified. Internet Explorer is not supported.