xxdom.js is a zero-dependency, single-file JavaScript library for declarative DOM rendering. No build step. No virtual DOM. No complexity. Just add a script tag and go.
<!-- That's it. No build step. --> <script src="https://unpkg.com/xxdom/dist/xx.min.js"></script> <div xx-scope="{ name: 'World' }"> <input xx-model="name"> Hello {{name}}! </div>
Familiar xx-for, xx-if, xx-model, xx-on syntax inspired by Vue.js. If you know Vue, you already know xxdom.
Use {{ "{" + "{expression}" + "}" }} anywhere in your HTML. Full JavaScript expressions — math, functions, ternaries, whatever you need.
Smart change detection updates only what changed. Key-based list diffing preserves DOM state across reorders.
Define reusable xx-component templates. Attribute merging, class concatenation, and dynamic template selection via xx-tmpl.
xx-init, xx-mounted, xx-updated, xx-destroy — hook into every stage of an element's life.
xx-await with xx-pending, xx-then, xx-catch slots. Handle promises declaratively without callback spaghetti.
xx(), it renders. No hidden reactivity magic, no stale closure surprises, no "why did this re-render?".
xx() manually after data changes. This is a deliberate trade-off for simplicity, but requires discipline.
Function() constructor for expressions, which requires unsafe-eval. Not suitable for strict Content Security Policy environments.
<slot> equivalent.
Real, measured performance across standard framework benchmark criteria. xxdom.js numbers are from our reproducible benchmark suite. Other framework data sourced from js-framework-benchmark (keyed, Chrome 120+).
Lower is better. Bars scaled relative to slowest framework in each category.
Every criterion that matters, side-by-side. Scroll horizontally on mobile.
| Criterion | xxdom.js | Alpine.js | Petite-Vue | Vue 3 | Svelte 5 | Lit | HTMX | Stimulus | Vanilla JS |
|---|---|---|---|---|---|---|---|---|---|
| Size & Setup | |||||||||
| Min + Gzip | 5.2 KB | 14.4 KB | 6.4 KB | 33 KB | 2.1 KB* | 5.8 KB | 14 KB | 7 KB | 0 KB |
| Dependencies | 0 | 0 | 0 | 3 | 0 | 0 | 0 | 2 | 0 |
| Build step | No | No | No | Yes | Yes | No | No | Optional | No |
| CDN script-tag install | Yes | Yes | Yes | Partial | No | Yes | Yes | Yes | Yes |
| Runtime Performance (keyed, 1K rows) | |||||||||
| Create 1,000 rows | ~19 ms | ~45 ms | ~32 ms | ~12 ms | ~5 ms | ~8 ms | server | N/A | ~3 ms |
| Replace all rows | ~7 ms | ~50 ms | ~35 ms | ~14 ms | ~5 ms | ~9 ms | server | N/A | ~3 ms |
| Partial update (10th row) | ~5 ms | ~18 ms | ~8 ms | ~6 ms | ~2 ms | ~3 ms | server | N/A | ~1 ms |
| Select row | ~4 ms | ~12 ms | ~5 ms | ~2 ms | ~1 ms | ~1 ms | server | N/A | <1 ms |
| Swap rows | ~4 ms | ~15 ms | ~7 ms | ~3 ms | ~1 ms | ~2 ms | server | N/A | <1 ms |
| Remove row | ~4 ms | ~14 ms | ~6 ms | ~2 ms | ~1 ms | ~2 ms | server | N/A | <1 ms |
| Clear all rows | ~1.4 ms | ~8 ms | ~4 ms | ~3 ms | ~1 ms | ~2 ms | server | N/A | <1 ms |
| Create 10,000 rows | ~210 ms | ~520 ms | ~360 ms | ~150 ms | ~65 ms | ~95 ms | server | N/A | ~40 ms |
| Startup time | ~3 ms | ~8 ms | ~5 ms | ~20 ms | ~2 ms | ~3 ms | ~6 ms | ~5 ms | 0 ms |
| Memory | |||||||||
| Ready memory (baseline) | ~1.2 MB | ~1.8 MB | ~1.4 MB | ~3.5 MB | ~0.8 MB | ~1.0 MB | ~1.5 MB | ~1.3 MB | ~0.5 MB |
| After 1K rows | ~5 MB | ~8 MB | ~6 MB | ~7 MB | ~3 MB | ~3.5 MB | varies | N/A | ~2.5 MB |
| DX & Architecture | |||||||||
| Reactivity model | Manual xx() |
Proxy | Proxy | Proxy | Compiled (runes) | Lit reactive props | Server-driven | Controller-based | Manual |
| Approach | Inline directives | Inline directives | Inline directives | SFC / templates | Compiled SFC | Web Components | HTML attributes | Data controllers | Imperative |
| TypeScript support | No types | Plugin | No | Full | Full | Full | No | Full | N/A |
| DevTools extension | No | Yes | No | Yes | Yes | Chrome | Extension | No | N/A |
| Plugin / extension system | No | Yes | No | Yes | Actions | Decorators | Yes | No | N/A |
| Router built-in | No | No | No | Vue Router | SvelteKit | No | hx-boost | Turbo | N/A |
| Keyed list diffing | Yes | Yes | Yes | Yes | Yes | Yes | server | N/A | DIY |
| Async / Suspense | xx-await | No | No | Suspense | await blocks | DIY | hx-indicator | No | DIY |
| SSR / pre-rendering | Yes (jsdom) | No | No | Nuxt | SvelteKit | SSR built-in | Server-first | Rails SSR | DIY |
| CSP compatible | No | Yes (v3) | No | Yes | Yes | Yes | Yes | Yes | Yes |
| Source LOC | ~1,373 | ~5,000 | ~2,000 | ~40,000 | compiler | ~6,000 | ~3,500 | ~2,000 | N/A |
| Ecosystem & Community | |||||||||
| npm weekly downloads | New | ~350K | ~15K | ~4.5M | ~1.2M | ~300K | ~200K | ~100K | N/A |
| Learning curve | ~1 hour | ~1 hour | ~1 hour | ~1 week | ~2 days | ~2 days | ~1 hour | ~2 days | Already known |
| Hiring / job market | None | Niche | None | Strong | Growing | Niche | Niche | Rails | Universal |
* Svelte compiles away at build time; runtime size varies by app. HTMX and Stimulus delegate rendering to the server, so client-side DOM benchmarks don't directly apply. Numbers for other frameworks are approximate ranges from js-framework-benchmark (keyed, Chrome 120+). xxdom.js numbers are measured on this machine — run them yourself.
▶ Run the Full Benchmark Suite in Your BrowserMinified + gzipped transfer sizes. How much JavaScript are you shipping?
This entire accordion is built with xx-for over a data array + xx-if to toggle answers.
Every feature that lightweight inline-directive frameworks offer, mapped across the landscape. This is the table you wish existed when choosing a framework.
| Feature / Directive | xxdom.js | Alpine.js | Petite-Vue | Vue 3 | Svelte 5 | Lit | HTMX |
|---|---|---|---|---|---|---|---|
| Templating & Rendering | |||||||
Text interpolation {{ "{" + "{}" + "}" }} |
✓ | ✗ | ✓ | ✓ | ✓ | Lit html | ✗ |
Text binding x-text |
xx-text | x-text | v-text | v-text | implicit | implicit | ✗ |
HTML binding x-html |
xx-html | x-html | v-html | v-html | {@html} | unsafeHTML | hx-swap |
Show / hide x-show |
✗ | x-show | v-show | v-show | CSS class | CSS class | ✗ |
Conditional x-if |
xx-if | x-if | v-if | v-if | {#if} | JS in template | ✗ |
| Else / else-if | ✗ | ✗ | v-else | v-else-if | {:else} | ternary | ✗ |
Loop x-for |
xx-for | x-for | v-for | v-for | {#each} | repeat() | ✗ |
| Keyed iterations | xx-for :key | :key | :key | :key | (item.id) | key fn | server |
| Data & Reactivity | |||||||
Scoped state x-data |
xx-scope | x-data | v-scope | data() | $state | properties | ✗ |
Two-way binding x-model |
xx-model | x-model | v-model | v-model | bind: | manual | ✗ |
| Computed / derived values | JS expression | getter in data | computed() | computed() | $derived | getter | ✗ |
| Watchers / effects | ✗ | x-effect | v-effect | watch() | $effect | updated() | ✗ |
| Global store | global vars | Alpine.store | shared obj | Pinia | stores | context | ✗ |
| Events & Attribute Binding | |||||||
Event binding x-on |
xx-on | x-on / @ | v-on / @ | v-on / @ | on: | @event | hx-trigger |
| Event modifiers (.prevent, .stop) | .prevent .stop | .prevent .stop .self .once | .prevent .stop | full set | |preventDefault | ✗ | limited |
| Key modifiers (.enter, .escape) | .enter .esc etc | .enter .escape etc | .enter .esc | full set | manual | ✗ | ✗ |
Attribute binding x-bind |
xx-attr | x-bind / : | v-bind / : | v-bind / : | implicit | implicit | hx-vals |
| Class binding (object syntax) | xx-class | x-bind:class | :class | :class | class: | classMap() | ✗ |
| Style binding (object syntax) | xx-style | x-bind:style | :style | :style | style: | styleMap() | ✗ |
| Property binding | xx-prop | x-bind | v-bind | .prop | implicit | .prop | ✗ |
| Components & Lifecycle | |||||||
| Components / templates | xx-component | Alpine.data | functions | SFC | SFC | LitElement | ✗ |
| Slots / content projection | ✗ | ✗ | ✗ | <slot> | <slot> | <slot> | ✗ |
| Init hook | xx-init | x-init | @mounted | onMounted | onMount | connectedCallback | ✗ |
| Mounted hook | xx-mounted | x-init (same) | @mounted | onMounted | onMount | firstUpdated | ✗ |
| Updated hook | xx-updated | ✗ | ✗ | onUpdated | afterUpdate | updated() | ✗ |
| Destroy hook | xx-destroy | x-destroy (Alpine.js 3.14+) | ✗ | onUnmounted | onDestroy | disconnectedCallback | ✗ |
| Advanced Features | |||||||
| Async / Suspense | xx-await | ✗ | ✗ | Suspense | {#await} | until() | hx-indicator |
| Transitions / animations | ✗ | x-transition | ✗ | Transition | transition: | CSS only | hx-swap transition |
| Element ref | $el in scope | x-ref / $refs | $refs | ref() | bind:this | @query | ✗ |
| Teleport / portal | ✗ | x-teleport | ✗ | Teleport | portal action | ✗ | hx-target |
| Cloak (hide until ready) | ✗ | x-cloak | v-cloak | v-cloak | compiled | shadow DOM | ✗ |
| Ignore subtree | ✗ | x-ignore | ✗ | v-pre | N/A | N/A | ✗ |
| Dynamic template selection | xx-tmpl | ✗ | ✗ | :is | svelte:component | manual | ✗ |
| Render batching | xx.render() | $nextTick | $nextTick | nextTick | tick() | updateComplete | N/A |
✓ = Supported ~ = Partial / workaround ✗ = Not available This matrix covers the inline-directive landscape most relevant to xxdom.js. React, Angular, and Solid use fundamentally different paradigms (JSX/templates) and are not directly comparable directive-by-directive.
Recurring themes from Reddit, GitHub discussions, and framework comparison threads. Here's how xxdom.js addresses (or honestly doesn't address) each one.
The #1 most common request. Developers working with static sites, WordPress, Django, Rails, or PHP templates want to sprinkle in reactivity without npm, webpack, or Vite. xxdom.js was built exactly for this: one <script> tag and you're done.
Many developers love Alpine's DX but find its 14KB payload and proxy-based reactivity harder to debug than expected. xxdom.js is 3x smaller and uses explicit xx() calls — no proxy magic, no hidden re-renders, no stale closure surprises.
A surprising number of developers want to fully understand their dependencies. xxdom.js is 1,373 lines of well-structured JavaScript — readable in a single sitting. Try doing that with Vue (40K LOC) or even Alpine (5K LOC).
xxdom solves thisMany developers reach for x-show (toggle display via CSS) as a lighter alternative to x-if (DOM insertion/removal). xxdom.js currently lacks this — you can use xx-style="{display: show ? '' : 'none'}" as a workaround.
Alpine's x-transition and Vue's <Transition> are highly requested features. xxdom.js doesn't have built-in transitions yet. You can use CSS transitions on elements, but there's no automatic enter/leave animation support.
Enterprise and government sites often require strict Content Security Policy. xxdom.js uses Function() for expression evaluation, which requires unsafe-eval. This is a fundamental design trade-off for inline expression convenience.
TypeScript is the #1 requested DX feature across all frameworks. xxdom.js doesn't ship type definitions yet. Since expressions are evaluated at runtime via Function(), full type checking of inline expressions isn't feasible — but type defs for the API are possible.
Currently you need separate xx-if blocks with inverted conditions. An xx-else directive would reduce duplication and improve readability. This is actively being considered.
This is the most polarizing request. Proxy-based auto-reactivity (like Alpine/Vue) eliminates manual xx() calls but adds complexity, can cause unexpected re-renders, and makes debugging harder. xxdom chose explicit rendering deliberately — it's a feature, not a limitation.
One line. That's the entire "install".
<script src="https://unpkg.com/xxdom/dist/xx.min.js"></script>
Use directives directly in your HTML. No compilation needed.
No dev server needed. Double-click the HTML file. It just works.
npm install xxdom
// CommonJS
const xx = require('xxdom');
// ES Modules
import xx from 'xxdom/dist/xx.esm.js';
xxdom.js is open-source and welcoming contributors. Whether you find a bug, add a feature, or just star the repo — every bit helps.
Ways you can help: