Jesse A. Green / notebook

Live demo: the DocCompiler guide library as a static, read-only snapshot (October 2026). No backend, no accounts, nothing to submit.

htmx Admin & Architecture Guide

Generated June 2026 from bigskysoftware/htmx at 5e867de

AI-generated documentation. The library is checked by sampling (three repositories so far); this guide was not individually reviewed. Verify against the project before relying on it.

This guide is for architects, platform engineers, and contributors who need to understand how htmx works internally, how to configure and secure it in production, and how the project is built and tested. It covers the library's architecture, the complete configuration surface, security model, build pipeline, and contribution workflow — written from the source at version 2.0.10.

The companion User Guide documents the hx-* attribute vocabulary for application developers. This guide focuses on what runs under those attributes and how to operate htmx safely at scale.


1. Executive Summary

htmx is a client-side JavaScript library that generalizes the hypermedia model of HTML: it lets any element issue AJAX requests and swap the resulting HTML into the DOM, declaratively, via HTML attributes. There is no client-side application state to manage, no JSON serialization layer, and no build step.

Property Value
Version (this analysis) 2.0.10
Source commit 5e867de (2026-06-01)
License 0BSD (BSD Zero Clause — public-domain-equivalent)
Package name htmx.org
Language JavaScript (single file), with TypeScript definitions generated from JSDoc
Runtime dependencies None
Distribution CDN (unpkg, jsDelivr), npm, or self-hosted
Homepage https://htmx.org/

Architectural thesis: htmx is a single ~5,600-line IIFE (immediately-invoked function expression) that registers a global event-delegation system. On load, it scans the DOM for hx-* attributes, wires up triggers, and intercepts the resulting events to perform XHR requests and DOM swaps. Everything else — history, indicators, out-of-band swaps, view transitions — is built on that core loop.

Note

For operators, the most important fact about htmx is that it shifts complexity to the server. There is no client build artifact to manage beyond a single static .js file, but your server now returns HTML fragments and must understand the HX-* header protocol. Treat the server as the source of truth and the place where security boundaries are enforced.


2. Repository Structure

The repository contains the library itself, its documentation website, editor tooling, and an extensive test suite.

htmx/
├── src/
│   ├── htmx.js              # The entire library (one IIFE, ~5,600 lines)
│   └── htmx.esm.d.ts        # Hand-maintained / generated TypeScript definitions
├── dist/                    # Build output (not in source tree; produced by scripts/dist.sh)
│   ├── htmx.js              # UMD build
│   ├── htmx.min.js          # Minified (CDN entry: jsdelivr/unpkg)
│   ├── htmx.esm.js          # ES module (package "main")
│   └── htmx.esm.d.ts        # Generated type declarations (package "types")
├── editors/
│   └── jetbrains/
│       └── htmx.web-types.json   # Web-Types metadata for JetBrains IDE autocomplete
├── scripts/                 # Build & release shell/Node scripts (dist.sh, www.sh, sha.sh, ...)
├── test/
│   ├── attributes/          # Per-attribute behavior tests (32 files)
│   ├── core/                # Core engine tests (17 files: api, ajax, events, ...)
│   ├── util/                # Test utilities
│   ├── lib/                 # Vendored test libs (e.g. hyperscript)
│   └── manual/              # Browser-driven manual test pages (history, hx-boost, ...)
└── www/                     # The htmx.org website (Hugo static site)
    ├── content/             # Markdown docs: attributes/, examples/, essays/, posts/, extensions/
    ├── templates/           # Hugo templates and shortcodes
    ├── themes/htmx-theme/   # The site theme
    └── static/              # Static assets (and a mirrored copy of the test suite)

Important

The www/static/test/ tree is a mirror of the top-level test/ directory, served by the website so tests can be run in the browser at htmx.org. When contributing, edit the canonical test/ files — the static copy is generated/synced, not authored directly.

Directory Role Audience
src/ The library source — the only file that ships Contributors
dist/ Build artifacts consumed by users Release
test/ Automated test suite (Mocha + Chai, run via web-test-runner) Contributors
www/ Documentation site (htmx.org) Docs contributors
editors/ IDE integration metadata Tooling
scripts/ Build, type-generation, and release automation Maintainers

3. Architecture Deep Dive

3.1 The IIFE and the public object

The entire library is wrapped in var htmx = (function() { 'use strict'; ... })(). Inside, a single public object is constructed and returned. Its shape (from the source) is:

htmx = {
  // Event processing
  onLoad, process, on, off, trigger, ajax,
  // DOM querying helpers
  find, findAll, closest, values,
  // DOM manipulation helpers
  remove, addClass, removeClass, toggleClass, takeClass, swap,
  // Extension entry points
  defineExtension, removeExtension,
  // Debugging
  logAll, logNone, logger,
  // Configuration
  config: { ... },
  // Misc
  parseInterval, location, _ (internalEval), version
}

A quirk worth knowing for contributors: the public methods are declared as null in the object literal and reassigned immediately afterward (htmx.process = processNode, etc.). The inline comment explains this is a TypeScript-compiler workaround — assigning functions directly produced invalid .d.ts output, but reassignment is fine.

3.2 The internal API

A second object, internalAPI, is exposed to extensions (via the init(api) callback). It surfaces lower-level functions that extensions need but that are not part of the stable public surface:

internalAPI = {
  addTriggerHandler, bodyContains, canAccessLocalStorage, findThisElement,
  filterValues, swap, hasAttribute, getAttributeValue, getClosestAttributeValue,
  getClosestMatch, getExpressionVars, getHeaders, getInputValues, getInternalData,
  getSwapSpecification, getTriggerSpecs, getTarget, makeFragment, mergeObjects,
  makeSettleInfo, oobSwap, querySelectorExt, settleImmediately, shouldCancel,
  triggerEvent, triggerErrorEvent, withExtensions
}

Warning

The internalAPI is intentionally not documented as a public contract. Extensions that reach into it are coupling to htmx internals and may break across minor versions. Prefer the documented extension hooks (onEvent, transformResponse, handleSwap, isInlineSwap, encodeParameters, getSelectors).

3.3 Node processing and per-element state

When htmx processes a node (processNode), it:

  1. Reads hx-trigger (or infers a default trigger from the element type).
  2. Parses trigger specifications into structured objects (HtmxTriggerSpecification).
  3. Registers event listeners that, when fired, issue the AJAX request.
  4. Computes an attribute hash (attributeHash) so it can detect when an element's attributes change and avoid re-initializing unchanged nodes.

Per-element private state (in-flight XHR, timeouts, listener registrations, polling state, the request queue, etc.) is stored on the element under a non-enumerable htmx-internal-data property, retrieved via getInternalData(elt). This keeps htmx's bookkeeping off the public DOM surface.

3.4 Attribute inheritance

Almost every hx-* attribute is inherited by descendants. Resolution walks up the ancestor chain (getClosestMatch / getClosestAttributeValue) until it finds the attribute. Two attributes modulate this:

  • hx-disinherit="<attrs>" — a parent declares which attributes its descendants should not inherit.
  • hx-inherit="<attrs>" — used when htmx.config.disableInheritance is true to opt specific attributes back into inheritance.

Setting htmx.config.disableInheritance = true flips the model from opt-out to opt-in across the whole page.

3.5 The request → swap pipeline

issueAjaxRequest(verb, path, elt, event)
  ├─ resolve target (hx-target) and swap spec (hx-swap)
  ├─ gather input values (form data, hx-include, hx-params filter, hx-vals)
  ├─ assemble headers (getHeaders → HX-Request, HX-Trigger, HX-Target, ...)
  ├─ htmx:confirm / htmx:configRequest / htmx:beforeRequest  (cancelable)
  ├─ open XMLHttpRequest, add htmx-request class
  ├─ on response → handleAjaxResponse:
  │     ├─ honor response headers (HX-Location, HX-Redirect, HX-Refresh,
  │     │   HX-Retarget, HX-Reswap, HX-Reselect, HX-Trigger*)
  │     ├─ resolve response handling (status code → swap? error?)
  │     ├─ htmx:beforeSwap  (can re-target / rewrite content / cancel)
  │     ├─ saveCurrentPageToHistory (if a history update is pending)
  │     ├─ swap(target, content, swapSpec)  → swapWithStyle dispatch
  │     │     ├─ OOB swaps (hx-swap-oob)
  │     │     ├─ title update, scroll/show, focus & selection preservation
  │     │     └─ settle phase (htmx-settling, attribute settling)
  │     └─ push/replace URL into history (if configured)
  └─ htmx:afterRequest, remove htmx-request class

3.6 Swap-style dispatch

swapWithStyle() is a switch over the swap style. The native styles are none, outerHTML, afterbegin, beforebegin, beforeend, afterend, delete, and innerHTML (the default fallthrough). textContent is handled earlier in swap() (it inserts text without HTML parsing). Any unrecognized swap style is offered to registered extensions' handleSwap hooks; if no extension claims it, htmx falls back to htmx.config.defaultSwapStyle. This is the extension point used by morphing extensions (e.g. Idiomorph).

3.7 Response handling configuration

The status-code → behavior mapping is data-driven via htmx.config.responseHandling, which defaults to:

responseHandling: [
  { code: '204', swap: false },                  // No Content: never swap
  { code: '[23]..', swap: true },                // 2xx/3xx: swap
  { code: '[45]..', swap: false, error: true }   // 4xx/5xx: error, no swap
]

Each entry can additionally specify target, select, swapOverride, ignoreTitle, and a client event to fire. This lets operators define application-wide policies (e.g. "swap 422 validation responses into the form") without per-element attributes.

Tip

Customizing responseHandling is the cleanest way to make error responses (like 422 Unprocessable Entity from server-side form validation) swap their body into the page, instead of being silently dropped as errors.


4. Configuration Reference

All runtime configuration lives on htmx.config. Set it in a script after htmx loads, or declaratively with a meta tag:

<meta name="htmx-config" content='{"defaultSwapStyle":"outerHTML","timeout":5000}'>

The following table is the complete config surface with the exact defaults from the source.

Key Type Default Purpose
historyEnabled boolean true Enable browser-history support
historyCacheSize number 10 Pages kept in sessionStorage for history restore
refreshOnHistoryMiss boolean false On a history cache miss, do a full reload instead of an AJAX fetch
defaultSwapStyle string 'innerHTML' Swap style when hx-swap is omitted
defaultSwapDelay number 0 ms between response and swap
defaultSettleDelay number 20 ms between swap and settle
includeIndicatorStyles boolean true Inject CSS so .htmx-indicator elements hide until a request runs
indicatorClass string 'htmx-indicator' Class revealed during requests
requestClass string 'htmx-request' Class added to the triggering element in-flight
addedClass string 'htmx-added' Class briefly applied to newly-added nodes
settlingClass string 'htmx-settling' Class on targets during settle
swappingClass string 'htmx-swapping' Class on targets during swap
allowEval boolean true Allow eval-based features (hx-vars, trigger filters, js: vals)
allowScriptTags boolean true Execute <script> tags in swapped content
inlineScriptNonce string '' CSP nonce added to inline scripts htmx re-inserts
inlineStyleNonce string '' CSP nonce added to inline styles
attributesToSettle string[] ['class','style','width','height'] Attributes transitioned during settle
withCredentials boolean false Send cookies/auth headers on cross-site requests
timeout number 0 Request timeout in ms (0 = no timeout)
wsReconnectDelay string | fn 'full-jitter' WebSocket reconnect backoff strategy
wsBinaryType string 'blob' Binary type for WebSocket data
disableSelector string '[hx-disable], [data-hx-disable]' Selector that disables htmx on a subtree
scrollBehavior string 'instant' Behavior for show/scroll (auto/instant/smooth)
defaultFocusScroll boolean false Scroll focused elements into view after a swap
getCacheBusterParam boolean false Add a cache-buster param to GET requests
globalViewTransitions boolean false Use the View Transitions API for all swaps
methodsThatUseUrlParams string[] ['get','delete'] Verbs that encode params in the URL
selfRequestsOnly boolean true Block requests to non-origin hosts
ignoreTitle boolean false Never update document.title from responses
scrollIntoViewOnBoost boolean true Scroll a boosted target into view
triggerSpecsCache object | null null Optional cache for parsed trigger specs
disableInheritance boolean false Flip attribute inheritance to opt-in
responseHandling array (see §3.7) Status-code → swap/error policy
allowNestedOobSwaps boolean true Process OOB swaps nested inside the main response
historyRestoreAsHxRequest boolean true Send HX-Request on history-miss full reloads
reportValidityOfForms boolean false Report HTML5 validation errors and focus the first invalid input

Note

Several defaults changed between htmx 1.x and 2.x. The most operationally significant is selfRequestsOnly now defaults to true (it was false in 1.x), hardening the default posture against cross-origin request abuse.


5. Security Analysis

htmx executes server-returned HTML in the browser, so the trust boundary is the server response. The library's security posture is governed primarily by a handful of config flags.

5.1 Threat model

Concern Mechanism Default posture
Cross-origin request abuse selfRequestsOnly Blocked by default (true)
eval / code injection allowEval Enabled (true) — needed for hx-vars, trigger filters, js: vals
Script execution in responses allowScriptTags Enabled (true) — <script> in swapped HTML runs
CSP compatibility inlineScriptNonce, inlineStyleNonce Empty — set to your per-request nonce
Credential leakage withCredentials Disabled (true sends cookies cross-site)
Sensitive data in history cache hx-history="false" Per-page opt-out

5.2 Content Security Policy (CSP)

htmx is CSP-friendly but requires configuration:

  • script-src without unsafe-eval: set htmx.config.allowEval = false. This disables hx-vars, square-bracket trigger event filters, and js:/javascript: prefixes in hx-vals/hx-headers. When eval is disallowed, htmx fires an htmx:evalDisallowedError event rather than evaluating.
  • Nonce-based inline scripts/styles: set inlineScriptNonce / inlineStyleNonce to the nonce your server emits per response. htmx copies the nonce onto any inline <script>/<style> it re-inserts during a swap (it must re-create script nodes because some browsers won't execute scripts inserted from <template> content).

Caution

allowScriptTags defaults to true, meaning any <script> tag in a swapped response will execute. If your server can ever reflect untrusted user content into an htmx response, you have a stored-XSS vector. Either rigorously sanitize/escape on the server, or set htmx.config.allowScriptTags = false to strip scripts from swapped content.

5.3 Cross-origin requests

With selfRequestsOnly = true (the default), htmx validates the request URL against the page origin and refuses non-same-origin requests, firing htmx:invalidPath/htmx:badResponseUrl paths as appropriate. Only relax this if you have a deliberate cross-origin integration, and pair it with a strict server-side CORS policy.

5.4 The HX-* header protocol as a security tool

Because htmx sends HX-Request: true on every request, servers can:

  • Distinguish AJAX fragment requests from full-page navigations and avoid leaking partial templates to direct URL access.
  • Use HX-Redirect/HX-Location to enforce auth redirects from the server without trusting client logic.

Tip

Validate the HX-* request headers server-side but never trust them for authorization — they are set by the client and can be forged. Use them for response shaping (fragment vs. full page), not for access control. Authorization must use your normal session/token mechanisms.

5.5 Known community-reported issues

From the project's issue tracker at the analyzed commit, security-relevant or behavior-edge issues to be aware of:

Issue Summary
#3743 Nested OOB swap performed even when nested OOB is intended to be off (interacts with allowNestedOobSwaps)
#3189 With HX-Redirect, the hx-indicator element is not deactivated
#2131 WebSocket: handling events with hx-on reported broken in an older version

These are functional edge cases rather than vulnerabilities, but operators relying on OOB-swap nesting behavior or redirect-with-indicator flows should test against them.


6. Build & Development Workflow

htmx ships a single source file but has a small Node-based toolchain for distribution, type generation, and the website. All commands are npm scripts defined in package.json.

6.1 Prerequisites

Tool Why
Node.js + npm Run scripts, ESLint, TypeScript, uglify
Playwright browsers Headless Chromium/Firefox/WebKit for the test suite
Bash Several scripts (dist.sh, www.sh, sha.sh) are shell scripts
git clone https://github.com/bigskysoftware/htmx.git
cd htmx
npm install
added N packages in Xs

6.2 Key npm scripts

Script Command (effect)
npm run dist scripts/dist.sh → build dist/, then generate types and web-types
npm run lint ESLint over src/htmx.js, test/, and scripts/*.mjs (config standard + mocha)
npm run format ESLint --fix over the same files
npm run types-check tsc type-check of src/htmx.js (--checkJs, no emit)
npm run types-generate Generate dist/htmx.esm.d.ts from the built ESM file
npm run web-types-generate node scripts/generate-web-types.mjs → JetBrains web-types JSON
npm test lint + types-check + test:chrome
npm run test:chrome / test:firefox / test:webkit Run the suite in a single browser via Playwright
npm run test:all Run across Chromium, Firefox, and WebKit
npm run test:ci lint + types-check + test:all (the full gate)
npm run www bash scripts/www.sh → build/serve the documentation site
npm run sha / update-sha Compute / update integrity SHAs for releases

Important

htmx is authored in plain JavaScript with JSDoc annotations, and TypeScript runs in check-only mode (--checkJs) against the source. The shipped .d.ts is generated from the built ESM file, not hand-written from scratch. When you change a public function signature, update its JSDoc — that is the source of truth for the generated types.

6.3 ESLint configuration

The project extends eslint-config-standard plus plugin:mocha/recommended, with a deliberately relaxed rule set (many no-* rules disabled) reflecting htmx's terse, performance-oriented style. Two rules are notable:

  • space-before-function-paren: ['error', 'never'] — function declarations have no space before the paren.
  • no-restricted-properties bans String.prototype.substr in favor of .slice/.substring.

7. Testing Strategy

htmx has an unusually thorough browser-based test suite, which is a key reason it can ship a single un-modularized file with confidence.

Aspect Detail
Frameworks Mocha (runner) + Chai / chai-dom (assertions) + Sinon (spies/stubs)
Execution @web/test-runner driving real browsers via Playwright
Browsers Chromium, Firefox, WebKit
WebSocket tests mock-socket for unit tests; a real ws server (test/ws-sse/server.js) for integration (npm run ws-tests)
Layout test/attributes/ (one file per hx-* attribute), test/core/ (engine: ajax, api, events, history, regressions), test/manual/ (human-driven browser pages for history & boost edge cases)

Tip

The test/manual/ pages exist because some behaviors — browser history restoration, hx-boost navigation, Safari/iOS history quirks — are difficult to assert in a headless runner. When debugging history or boost issues, open these pages in a real browser; they reproduce the exact scenarios the maintainers use.

To run the full suite the way CI does:

npm run test:ci
> lint && types-check && test:all
... ESLint passes
... tsc type-check passes
... web-test-runner: N tests passed across chromium, firefox, webkit

8. Deployment & Operations

htmx itself is a static asset; "deployment" means how you serve and version it.

8.1 Serving strategies

Strategy Trade-offs
Public CDN (unpkg/jsDelivr) Zero hosting; relies on third-party availability and trust. Pin exact versions.
Self-hosted minified file Full control, same-origin, CSP-simple. You own cache headers and updates.
Bundled (npm import) Integrates with your build; ensure window.htmx is set if you use inline attributes.

8.2 Operational checklist

  • [ ] Pin an exact htmx version (avoid floating @latest / major-only tags).
  • [ ] Add Subresource Integrity (integrity + crossorigin) if loading from a CDN.
  • [ ] Decide and set the security flags: allowEval, allowScriptTags, selfRequestsOnly, nonces.
  • [ ] Define a responseHandling policy for error/validation status codes.
  • [ ] Ensure the server branches on the HX-Request header (fragment vs. full page).
  • [ ] Add hx-history="false" to any page caching sensitive data in sessionStorage.
  • [ ] Set a timeout if your backend can hang, and handle htmx:timeout.
  • [ ] Establish a CSP and verify htmx features still work under it.

Warning

If you serve htmx from a CDN without Subresource Integrity, a compromise of that CDN would let an attacker run arbitrary JavaScript on every page that includes htmx. SRI hashes (which the npm run sha script helps generate) pin the exact bytes you trust.

8.3 Performance characteristics

  • htmx adds work proportional to the number of hx-*-annotated elements processed on load and after each swap. It caches attribute hashes to skip unchanged nodes.
  • Trigger spec parsing can be cached via htmx.config.triggerSpecsCache if you have many elements sharing identical hx-trigger strings.
  • The library uses event delegation and XMLHttpRequest; there is no virtual DOM diffing overhead. The cost model is "parse a fragment, do native DOM insertion, settle a few attributes."

9. Editor & Tooling Integration

editors/jetbrains/htmx.web-types.json provides Web-Types metadata so JetBrains IDEs (WebStorm, IntelliJ, PyCharm) offer autocomplete and documentation for hx-* attributes. It is generated by scripts/generate-web-types.mjs and published as the package's web-types field, so consumers who npm install htmx.org get editor support automatically. For VS Code, equivalent support is provided by community extensions consuming the same metadata.


10. Extending htmx

Extensions are first-class. Register one with htmx.defineExtension(name, partialExtension); activate it on a subtree with hx-ext="name".

The extension interface (defaults shown by extensionBase()):

Hook Signature Purpose
init(api) (internalAPI) => void Receive the internal API on registration
onEvent(name, evt) (string, CustomEvent) => boolean Observe/veto any htmx event (return false to cancel)
transformResponse(text, xhr, elt) => string Rewrite the raw response before swapping
isInlineSwap(swapStyle) => boolean Declare a custom swap style as inline
handleSwap(swapStyle, target, fragment, settleInfo) => boolean | Node[] Implement a custom swap style (e.g. morph)
encodeParameters(xhr, params, elt) => any | string | null Custom request body encoding
getSelectors() => string[] | null Extra selectors htmx should process

In htmx 2, formerly-core features such as WebSockets (ws), Server-Sent Events (sse), and class-based response targeting moved into official extensions, keeping the core library small.

htmx.defineExtension('uppercase', {
  transformResponse: function (text /*, xhr, elt */) {
    return text.toUpperCase()
  }
})

11. Code Quality & Conventions

Observation Detail
Single-file design The whole library is one IIFE in src/htmx.js. This is intentional — it keeps the distributable a single small file and avoids a bundler.
Defensive DOM access Helpers like asElement, asHtmlElement, asParentNode, and getInternalData guard against null/wrong-type nodes throughout.
Type safety via JSDoc Extensive JSDoc @param/@returns/@typedef annotations drive tsc --checkJs and the generated .d.ts.
Terse style Many lint rules are relaxed; the code favors compactness. Contributors should match the surrounding style and run npm run format.
Shadow DOM aware getRootNode({composed}), parentElt (handles ShadowRoot), and extended selectors (host, root) show deliberate shadow-DOM support.
Browser-compat workarounds E.g. normalizeScriptTags re-creates <script> nodes because some browsers won't execute scripts inserted from <template> content.

12. Potential Improvements & Technical Debt

These are observations for maintainers and downstream forks, derived from the source and issue tracker — not defects in normal operation.

  • Single-file size: at ~5,600 lines the library is large for one file. Internal modularization (kept bundled for distribution) could improve contributor onboarding, at the cost of a build step the project currently avoids by design.
  • eval surface: allowEval-gated features (hx-vars, trigger filters, js: vals) are a recurring CSP friction point. Documenting a fully eval-free usage path prominently would help locked-down deployments.
  • OOB-swap nesting (#3743): the interaction between allowNestedOobSwaps and nested OOB elements has a reported surprising case worth a regression test and doc clarification.
  • Indicator lifecycle on redirect (#3189): hx-indicator not deactivating on HX-Redirect is a UX rough edge.
  • Release artifact consistency (#3765, #3553): community reports of CDN/GitHub release artifact mismatches suggest the release pipeline (scripts/) could benefit from tighter automation/verification.

13. Quick Reference

Security flags at a glance

Flag Harden by setting
allowEval false (strict CSP, no unsafe-eval)
allowScriptTags false (strip scripts from swapped HTML)
selfRequestsOnly keep true (default)
withCredentials keep false unless cross-site auth is required
inlineScriptNonce / inlineStyleNonce your per-request CSP nonce

Request headers htmx sends

HX-Request, HX-Trigger, HX-Trigger-Name, HX-Target, HX-Current-URL, HX-Prompt, HX-Boosted, HX-History-Restore-Request

Response headers htmx honors

HX-Location, HX-Redirect, HX-Refresh, HX-Push-Url, HX-Replace-Url, HX-Retarget, HX-Reswap, HX-Reselect, HX-Trigger, HX-Trigger-After-Swap, HX-Trigger-After-Settle

Build/test commands

npm install           # install dev toolchain
npm run dist          # build dist/ + types + web-types
npm test              # lint + type-check + chrome tests
npm run test:ci       # full gate: lint + type-check + all browsers
npm run www           # build/serve the docs site