idiomorph
A dependency-free JavaScript library that morphs one DOM tree into another using structural id-set matching for more stable merges.
Repository Health
Technical Analysis
Idiomorph is a small, dependency-free JavaScript library for morphing one DOM tree into another in place. Where earlier morphing libraries like morphdom and nanomorph match elements only by their own id attribute, Idiomorph computes an id set for every node - the union of all ids found anywhere within its subtree - and matches old and new elements when those sets intersect. This lets it correctly recognize that a deeply nested, stateful element (a video iframe, a focused input, a web component) has simply moved to a new position in the tree, rather than been replaced, even when the elements directly wrapping it have no ids of their own.
The library ships as a single small file with no runtime dependencies, usable directly via a script tag or as an ESM import, and includes TypeScript declarations generated from its JSDoc annotations. A companion extension file wires Idiomorph into htmx as a swap mechanism (hx-swap=“morph”), which is the project it was originally built to serve; it has since also been adopted by Turbo 8 and Datastar as their default morphing strategy.
What You Get
- A single Idiomorph.morph(oldNode, newContent, config) entry point that accepts a DOM node, an HTML string, an HTMLCollection, or an array of nodes as the new content
- Configurable morphStyle (outerHTML or innerHTML) to control whether the target node itself or only its children are replaced
- A full lifecycle callback system (beforeNodeAdded, afterNodeMorphed, beforeAttributeUpdated, etc.) that lets callers veto or observe individual mutations
- Dedicated head merging modes (merge, append, morph, none) plus per-element im-preserve / im-re-append attributes for fine-grained control over style/script tags
- Automatic focus and text-selection restoration after a morph, with ignoreActive / ignoreActiveValue escape hatches for in-progress form input
- A pre-built htmx extension (idiomorph-ext) enabling hx-swap=“morph” and hx-swap=“morph:innerHTML” as drop-in swap strategies
Common Use Cases
- Swapping htmx hx-swap targets so server-rendered fragment updates preserve focus, video/iframe playback, and CSS transition state instead of hard-replacing the DOM
- Powering full-page morphing in Turbo-style navigation, where an entire html document is re-rendered but only the parts that actually changed should be touched
- Merging a freshly fetched head (new stylesheets, meta tags, scripts) into the live page without flashing already-loaded assets
- Any hypermedia-driven or server-rendered app that needs SPA-like DOM stability (no flicker, preserved scroll/focus) without adopting a virtual DOM framework or client-side router
Under The Hood
Architecture Idiomorph is a single IIFE module (src/idiomorph.js, ~1,450 lines) that exposes one global object with a morph() entry point and a mutable defaults config. Internally it separates into clear phases: it first walks both the old and new trees bottom-up to build id sets (a Map of every id found within each node’s subtree, plus a global persistentIds set), then runs a recursive morphChildren pass that uses those id sets to decide, for each pair of children, whether to morph in place, move, insert, or remove - falling back to a hidden staging element (the pantry) to hold detached nodes that may be reinserted later in the same pass. Attribute and value synchronization for matched nodes is handled by a separate syncNodeFrom routine, and head merging is implemented as its own dedicated code path with four selectable strategies. The htmx integration (src/idiomorph-htmx.js, 24 lines) is a thin, fully decoupled extension registered against the htmx extension API, so the core morphing engine has no knowledge of htmx at all. Because the entire matching pipeline depends on the id sets computed up front, any change to how those sets are built would ripple through every downstream decision in morphChildren.
Tech Stack Zero runtime dependencies - the library is plain, framework-agnostic JavaScript distributed as a global script, an ESM module (dist/idiomorph.esm.js), and a minified UMD-style bundle via unpkg. TypeScript is used only as a type-checking and .d.ts-generation layer over JSDoc-annotated source (tsc —declaration —emitDeclarationOnly —allowJs), so consumers get full type definitions without the library itself going through a TS compile step. The build pipeline is a set of small npm scripts that concatenate source files, run uglify-js for minification, and gzip the output to track bundle size. htmx.org appears only as a devDependency, used exclusively to exercise the htmx extension in tests.
Code Quality The test suite (test/core.js, ops.js, hooks.js, head.js, preserve-focus.js, restore-focus.js, retain-hidden-state.js, htmx-integration.js, fidelity.js, realm.js) is extensive and runs via @web/test-runner with Mocha, Chai/Chai-DOM, and Sinon, executed against three real browser engines (Chromium, Firefox, WebKit) in separate CI jobs. CI also runs a dedicated full-coverage-enforcement script that fails the build if any line lacks test coverage, plus prettier —check and a standalone tsc typecheck pass. Naming throughout the source is consistent and domain-specific (idMap, persistentIds, pantry), and no error-swallowing was observed in the core morph path.
What Makes It Unique Idiomorph’s core innovation is its id set matching algorithm: instead of matching elements only by their own id attribute (as morphdom and nanomorph do), it computes, for every node, the union of ids found anywhere within its subtree, then matches old and new elements when those sets intersect. This lets it correctly recognize that a deeply nested stateful element - a live iframe, a focused input, a mounted widget - has simply moved within the tree, even when the elements wrapping it carry no ids of their own, a case where id-only matching detaches and loses the node. The project documents this trade-off honestly: benchmarks show it running roughly on par with or slightly behind morphdom on raw speed, in exchange for meaningfully better structural matching, which is the reason it has since been adopted as the default morphing strategy in Turbo 8 and Datastar.
Used by 2 apps in this directory
Gitea
Devops · Developer Tools · Project Management
Self-hosted DevOps in a single Go binary — Git hosting, GitHub Actions-compatible CI/CD, and 30+ package registries without any SaaS dependency.
OpenProject
Project Management · Productivity · Collaboration
The open source project management platform that unifies Gantt charts, agile boards, time tracking, and team collaboration under full self-hosted control.