Not every interactive page needs a framework. htmx 79,053 and Alpine.js 208,904 extend HTML with attributes (Lightweight DOM Libraries compares them with other lightweight DOM libraries); Cheerio 30,512 and jsdom 21,691 handle HTML without a browser; template engines generate it.
htmx
htmx (https://github.com/bigskysoftware/htmx 49,522 ) (0BSD licence) lets any element issue a request and swap the HTML response into the page. hx-get/hx-post choose the request, hx-trigger the event (a click for buttons by default), hx-target where the response goes and hx-swap how (innerHTML by default). The server returns HTML fragments, so there is no client-side rendering code. This complete app needs only Node:
import { createServer } from "node:http";
let likes = 0;
const page = () => `<!doctype html>
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.10/dist/htmx.min.js"></script>
<button hx-post="/like" hx-target="#count">Like</button>
<span id="count">${likes} likes</span>`;
createServer((req, res) => {
res.setHeader("Content-Type", "text/html; charset=utf-8");
if (req.method === "POST" && req.url === "/like") {
likes++;
return res.end(`<strong>${likes}</strong> likes`); // an HTML fragment, not JSON
}
res.end(page());
}).listen(3000);Run node server.mjs and open http://localhost:3000: each click sends POST /like with an HX-Request: true header and the span becomes 1 likes, 2 likes and so on. htmx 2.0.10 is npm 2,036 's latest; htmx 4.0 is released under the next tag so 2.x sites are not upgraded by accident.
Alpine.js
Alpine.js (https://github.com/alpinejs/alpine 31,940 ) (MIT) keeps state in the markup: x-data declares a reactive object, @click (short for x-on:click) handles events, x-text binds text and x-show toggles visibility. The script needs defer. The demo clicks both buttons once before the capture:
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.17.3/dist/cdn.min.js"></script>
<div x-data="{ qty: 1, open: false }" style="font: 16px sans-serif">
<button @click="qty++">Add one</button>
<button @click="open = !open" x-text="open ? 'Hide details' : 'Show details'"></button>
<p>Quantity: <strong x-text="qty"></strong>, total <strong x-text="'$' + qty * 39"></strong></p>
<p x-show="open">Desk lamp, $39 each, ships in 2 days.</p>
</div>
<script>
addEventListener("load", () => document.querySelectorAll("button").forEach((b) => b.click()));
</script>
Cheerio and jsdom
Cheerio (https://github.com/cheeriojs/cheerio 30,512 ) (MIT) parses HTML into a tree with a jQuery-style API. It runs no scripts, which makes it fast for scraping, rewriting static builds or checking generated markup:
import * as cheerio from "cheerio";
const $ = cheerio.load(`<ul>
<li class="item" data-price="39">Desk lamp</li>
<li class="item sale" data-price="25">Clip light</li>
</ul>`);
const items = $(".item").map((i, el) => `${$(el).text()}: $${$(el).data("price")}`).get();
console.log(items);
$(".sale").append(" (sale)").attr("data-price", "20");
console.log($.html(".sale"));[ 'Desk lamp: $39', 'Clip light: $25' ] <li class="item sale" data-price="20">Clip light (sale)</li>
jsdom (https://github.com/jsdom/jsdom 21,691 ) (MIT, version 30) instead implements the DOM and many Web APIs in JavaScript, with events and optional script execution (new JSDOM(html, { runScripts: "dangerously" })); Jest 62,362 and Vitest 102,989 use it to test DOM code. It has no layout engine, so use Playwright 26,280 when you need real rendering.
Template engines
A template engine merges data into HTML on the server or at build time. All four below escape <, > and & by default, blocking cross-site scripting; use the raw form only for trusted HTML.
| Engine | Loop over items | Raw output | Licence |
|---|---|---|---|
| Handlebars 131,260 (https://github.com/handlebars-lang/handlebars.js 18,679 ) | {{#each items}}{{this}}{{/each}} | {{{ html }}} | MIT |
| Nunjucks 126 (https://github.com/mozilla/nunjucks 9,000 ) | {% for i in items %}{{ i }}{% endfor %} | {{ html | safe }} | BSD-2-Clause |
| EJS 689,492 (https://github.com/mde/ejs 8,130 ) | <% items.forEach(i => { %><%= i %><% }) %> | <%- html %> | Apache-2.0 |
| Pug 352,080 (https://github.com/pugjs/pug 21,837 ) | each i in items then indented li= i | li!= html | MIT |
Handlebars keeps logic out of templates, Nunjucks (Jinja2-style) adds inheritance and macros, EJS is plain JavaScript inside tags, and Pug replaces tags with indentation.