How to Use HTMX: The Beginner's Guide (First Script Tag, First hx-get, the Server-Returns-HTML Rule, and First Form)
CodexFuse Team · 2026-10-31T13:43:17.628Z · 14 min read
Learn how to use htmx: the first script tag, your first hx-get, hx-target and hx-swap placement, the server-returns-HTML rule, your first hx-post form, spinners, and a real Express first project.
How to Use HTMX: The Beginner's Guide (First Script Tag, First hx-get, the Server-Returns-HTML Rule, and First Form)
Learning how to use htmx is the fastest way to build interactive web pages without writing JavaScript. htmx is a small, dependency-free JavaScript library — the current release is htmx 4.0.0 (published on GitHub on August 28, 2026) — that lets any HTML element issue HTTP requests straight from markup, using attributes that start with hx-. This htmx tutorial runs the complete first-project arc in one sitting: the first script tag (the htmx CDN), your first hx-get request, hx-target and hx-swap placement, the server-returns-HTML rule that every beginner must internalize, your first hx-post form, a loading spinner with hx-indicator, and a minimal Express backend you can ship in a container — our Docker beginner guide covers the container step if that is new to you. No build step, no bundler, no framework ceremony: one script tag and you are dangerous.
What is htmx?
htmx is a single-file JavaScript library that generalizes HTML's built-in hypermedia controls. Plain HTML only lets <a> and <form> make HTTP requests, only on click and submit events, and only with GET and POST. htmx extends that idea with custom attributes: any element can issue any HTTP request (GET, POST, PUT, PATCH, DELETE), any event can trigger it, and the response HTML can be placed anywhere in the page. The server returns small HTML fragments — not JSON — and htmx swaps them into the DOM. Because it works in terms of HTML, it follows the original web programming model (hypertext as the engine of application state), so it feels like traditional web development rather than a client-side framework. It ships as one file with zero dependencies, and there is deliberately no build step.
How do you use htmx? The 7-step first arc
Before the code, here is the whole arc so you can see where you are heading:
- Add the htmx script tag to your page (the htmx CDN, version pinned).
- Fire your first
hx-getrequest from a button. - Learn the server-returns-HTML rule — the one idea that makes htmx click.
- Place responses with
hx-targetandhx-swap. - Submit your first form with
hx-post(plus the sanitization rule). - Show a spinner with
hx-indicator. - Ship it: a tiny Express backend, a container, and a server.
Step 1: How do I add htmx to a page?
You add htmx with one script tag in the page's <head>, loaded from a CDN or a local file. The official docs for htmx 4 recommend jsDelivr with a pinned version and a subresource-integrity hash, so a tampered CDN copy cannot run on your page:
<script src="https://cdn.jsdelivr.net/npm/[email protected]"
integrity="sha384-BvJpBiO8Kh31EqtJe5DRIeWrHWnCGkwytKs9NKFi86Hhw96dEqdEMzZDeK9lF"
crossorigin="anonymous"></script>
That is the entire install — there is no build step and nothing to configure. Two alternatives worth knowing: npm install [email protected] followed by import 'htmx.org' if you already have a bundler, and vendoring (downloading htmx.min.js into your project's own /js/ folder and linking it locally), which the official docs recommend for production so your page does not depend on a third-party CDN at runtime. Either way, the rule beginners should follow is to pin the version — @4.0.0, not @latest — so a future major release cannot silently change behavior under you.
Step 2: What does hx-get do?
hx-get tells an element to issue an HTTP GET request to a URL when its natural event fires — for a button, that event is a click. This is the "hello world" of htmx:
<button hx-get="/greeting" hx-target="#hello">Say hello</button>
<div id="hello"></div>
Click the button and htmx sends a GET to /greeting in the background, takes the HTML the server returns, and inserts it into the #hello div — with no page reload. Notice the two attributes doing the work: hx-get="/greeting" is the request, and hx-target="#hello" is where the response lands, selected with a normal CSS selector. If you leave hx-target off, htmx targets the element itself. The same pattern works with the other request attributes — hx-post, hx-put, hx-patch, hx-delete, and (new in htmx 4) hx-query — and htmx 4 adds an alternative form-grammar pair, hx-action for the URL plus hx-method for the HTTP method, so a button can read like a form: <button hx-action="/info" hx-method="GET">.
Step 3: Why must the server return HTML?
The server-returns-HTML rule is the one idea that makes htmx click: htmx expects an HTML fragment from the server, not JSON. In the Step 2 example, the /greeting endpoint should return something like <p>Hello from the server — this is HTML, not JSON.</p>, and htmx inserts that markup directly. This is the deliberate opposite of the modern SPA habit of shipping JSON to the browser and rendering it with client-side JavaScript. The payoff is that your backend renders the final markup it already knows how to render, and the browser does almost no work.
This is also the number-one beginner trap: pointing htmx at an existing JSON API and wondering why the page shows raw text. If you genuinely must consume a JSON endpoint, you have to intercept the response and convert it yourself (the docs show hooking the htmx:after:request event for exactly this), but the idiomatic htmx move is to add a route that renders the fragment as HTML. Any backend works for this — Express, Django, Rails, PHP, Go — because the contract is just "return a snippet of HTML."
Step 4: Where does the response go? hx-target and hx-swap
hx-target picks the destination with a CSS selector, and hx-swap decides how the response is inserted. The default swap is innerHTML — the response replaces the target's contents. Here are the values you will actually use in a first project:
| hx-swap value | What it does | First-project use |
|---|---|---|
innerHTML (default) | Replaces the target's contents | Loading a greeting into an empty div |
outerHTML | Replaces the whole target element | A button that turns into its result after clicking |
beforeend | Appends the response inside the target | Adding a new todo item to the end of a list |
afterbegin | Prepends the response inside the target | Newest-first activity feeds |
beforebegin / afterend | Inserts before / after the target element | Inline validation messages next to a field |
textContent | Sets the target's text without parsing HTML | Showing a count or status string safely |
The todo-list case is worth seeing once, because it is the moment most beginners feel the paradigm shift — the server returns one <li> and htmx appends it:
<form hx-post="/todos" hx-target="#todo-list" hx-swap="beforeend">
<input name="task" placeholder="New task">
<button>Add</button>
</form>
<ul id="todo-list"></ul>
Step 5: How do I submit a form with htmx?
You submit a form with htmx by putting hx-post on the form (or its button): clicking submits the form's fields asynchronously and the page does not reload. Here is the complete pattern, including the sanitization rule that the popular crash-course walkthroughs flag — never inject raw user input into a response, because htmx inserts server HTML straight into the page:
// server.js — minimal Express backend for the form above
const express = require('express');
const app = express();
app.use(express.urlencoded({ extended: true }));
function escapeHtml(s) {
return String(s)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
app.post('/todos', (req, res) => {
const task = escapeHtml(req.body.task || '').trim();
if (!task) return res.send('<li class="error">Task cannot be empty.</li>');
res.send(`<li>${task}</li>`);
});
app.listen(3000, () => console.log('Listening on http://localhost:3000'));
The escapeHtml helper is the whole security lesson: htmx will faithfully insert whatever your server returns, so the server must escape user text before returning it. For real projects, use a battle-tested sanitizer library (the xss npm package is the one the traversymedia crash course uses) rather than a hand-rolled helper. Note that this example deliberately returns validation errors as HTML too — that is the htmx 4 behavior to be aware of: unlike htmx 2, htmx 4 swaps 400 and 500 responses by default, so returning an error status with an HTML body is a legitimate way to show inline errors (you can restore the old no-swap behavior with htmx.config.noSwap = [204, 304, '4xx', '5xx'] if you prefer).
Step 6: How do I show a spinner and control when requests fire?
A spinner in htmx is an element with the htmx-indicator class, which htmx automatically reveals while a request is in flight and hides when it completes. Point at it with hx-indicator on the action element:
<button hx-get="/report" hx-target="#output" hx-indicator="#spinner">
Generate report
</button>
<img id="spinner" class="htmx-indicator" src="/loading.gif" alt="Loading">
<div id="output"></div>
When a request should fire is controlled by hx-trigger. The defaults are sensible — input, textarea, and select fire on change, forms fire on submit, everything else fires on click — but the modifiers are where htmx earns its keep. The classic is live search: fire on each keystroke, but wait until the user pauses, and only if the value actually changed:
<input type="text" name="q" placeholder="Search..."
hx-get="/search"
hx-trigger="keyup changed delay:500ms"
hx-target="#search-results">
<div id="search-results"></div>
The modifiers you will reach for first, straight from the official htmx 4 docs:
| Modifier | What it does | Use it for |
|---|---|---|
changed | Only fires if the element's value changed | Search boxes, filters |
delay:500ms | Waits, and resets if the event fires again | Debouncing keystrokes |
throttle:2s | Fires immediately, then at most once per interval | Scroll and resize handlers |
once | Fires a single time, then never again | One-shot onboarding calls |
every 2s | Polls on a timer | Live dashboards, notification badges |
load / revealed | Fires on load / when scrolled into view | Lazy-loading content and images |
click[shiftKey] | Trigger filter: only when the expression is truthy | Shift-click confirmations for destructive actions |
One caution from the docs: trigger filters like click[shiftKey] are evaluated with eval(), so avoid them on pages running under a strict Content Security Policy.
Step 7: How do I ship it? The Express backend, a container, and a server
Your first htmx project is two files: an index.html with the script tag and your hx-* attributes, and a tiny server that serves it and returns HTML fragments. The Express example from Step 5 is the pattern — any backend that can return a snippet of HTML qualifies, which is why the ecosystem's beginner guides use Express, Go, Django, and PHP interchangeably. Serve the static page and the fragment routes from the same server, test the whole arc at http://localhost:3000, and then package it: put the server in a container with our Docker beginner guide, put the container on a cheap machine (a what is VPS hosting primer covers the options beginners actually buy), and terminate HTTPS in front of it with our Caddy beginner guide. The htmx part needs nothing special at deploy time — it is a static script tag, so your existing deployment knowledge transfers wholesale.
What changed in htmx 4, and should beginners care?
Beginners should start on htmx 4 — it is the current line, and the official docs ship an upgrade-checker (npx [email protected] upgrade-check -- ./path/to/project) for anyone migrating 2.x templates. The three behavioral changes that matter, documented on four.htmx.org:
| Behavior | htmx 2.x | htmx 4.0 |
|---|---|---|
| Attribute inheritance | Implicit by default (attributes flow down to children automatically) | Explicit by default — opt back in with htmx.config.implicitInheritance = true |
| 4xx / 5xx responses | Not swapped by default | Swapped by default — error HTML renders inline; restore with htmx.config.noSwap = [204, 304, '4xx', '5xx'] |
| History restoration | Local cache snapshot | Issues a request to the server for the full page; the hx-history-cache extension restores the old behavior |
| Event names | Older mixed naming | Standardized and rationalized |
| New request attributes | — | hx-query (HTTP QUERY), hx-action + hx-method form grammar |
| Extension syntax | hx-ws="connect:/chatroom" | Colon namespaced: hx-ws:connect="/chatroom", hx-ws:send, hx-boost:inherited="true" |
The inheritance change is the one that bites migrating developers: in htmx 2, an hx-target on a parent applied to children automatically; in htmx 4 you say so explicitly. For a brand-new project this is a non-issue — you simply learn the explicit form.
htmx vs React: which should you learn first?
This is the comparison every "htmx vs react" searcher is really asking, and the honest answer is that they solve different problems. The table below is the decision rule:
| Question | htmx is the better fit | React is the better fit |
|---|---|---|
| What kind of app? | Content sites, dashboards, admin panels, CRUD apps — the classic multi-page web | Highly interactive SPAs: editors, design tools, real-time collaboration canvases |
| Where does rendering happen? | On the server, as HTML fragments | In the browser, from JSON state |
| Build tooling | None — one script tag | Bundler, transpiler, and dependency tree required |
| Team skill | Backend developers who know HTML | Frontend specialists comfortable with component state |
| Failure mode | A broken fragment is one broken panel | A broken bundle can be a blank page |
| Learning curve | An afternoon if you know HTML and one backend | Weeks: JSX, hooks, state management, build config |
The practical test: if your app could be built as server-rendered pages with a few dynamic panels, htmx will get you there with a fraction of the code. If your app needs rich client-side state — drag-and-drop editors, offline-first sync, complex animations — that is what React is for. Plenty of teams use both: htmx for the content and admin surface, React (or a similar framework) for the one genuinely app-like screen.
What are the 5 beginner mistakes that break htmx pages?
Almost every "htmx is not working" thread on the beginner forums is one of these:
| Mistake | Symptom | The fix |
|---|---|---|
| Returning JSON from the endpoint | Raw text or nothing renders in the target | Return an HTML fragment; htmx swaps markup, not data |
Using @latest in the CDN tag | An upgrade silently changes behavior (e.g. 2.x → 4.x inheritance) | Pin the version: [email protected] |
Forgetting hx-target on a swap | The response replaces the button/form itself | Add hx-target with the destination's CSS selector |
| Injecting unsanitized user input | Stored XSS — another user's script runs in every visitor's browser | Escape on the server (or use a sanitizer like the xss package) |
| Expecting htmx to wire up manually-fetched HTML | Fragments added via your own fetch() do nothing | Call htmx.process() on manually inserted content |
Sources
Version and install facts verified October 31, 2026: htmx v4.0.0 released August 28, 2026 — GitHub bigskysoftware/htmx releases API; the jsDelivr CDN tag with integrity hash, the npm install command, core attributes (hx-get, hx-post, hx-put, hx-patch, hx-delete, hx-query), the new hx-action/hx-method grammar, default triggers, trigger modifiers and special events, swap values, and the three htmx 2.x → 4.x behavioral changes plus the npx [email protected] upgrade-check tool — all from the official htmx 4 documentation at four.htmx.org/docs (crawled October 31, 2026). Indicator, extended-target, and extension syntax (hx-ws:connect, hx-sse, hx-boost:inherited) from "Get started with htmx 4" — InfoWorld, published September 14, 2026, updated September 23, 2026. The live-search pattern (keyup changed delay:300ms) from "HTMX Tutorial: Building Modern Web Applications Without Heavy JavaScript Frameworks" — C# Corner (113 days old). The todo-list beforeend swap pattern and the XSS sanitization caution from "HTMX Tutorial – Dynamic HTML without JavaScript" — HayaGeek. htmx authorship (Carson Gross) from the DEV Community htmx explainers.
Frequently asked questions
Is htmx still relevant in 2026?
Yes — htmx 4.0.0 shipped in August 2026 with a modernized attribute model, standardized events, and an official migration tool, and publishers like InfoWorld are writing fresh "get started with htmx 4" guides. The underlying argument has not changed: most web apps are content with a few dynamic panels, and server-rendered HTML fragments remain the simplest way to build them.
Do I need Node.js to use htmx?
No. htmx is backend-agnostic — it only cares that your server returns HTML fragments. Express is used in this guide because it is the shortest path to a working example, but Django, Rails, Laravel, PHP, Go, and ASP.NET all work identically from htmx's point of view.
Can I use htmx with my existing JSON API?
Not directly — htmx swaps HTML into the page, so pointing it at a JSON endpoint shows raw text. The idiomatic fix is to add a route that renders the same data as an HTML fragment. If that is impossible, you can intercept the response with the htmx:after:request event and convert it, but that is swimming upstream.
Is htmx a replacement for React?
For content sites, dashboards, and CRUD apps, it replaces the need for React. For genuinely app-like interfaces — real-time collaborative editors, complex client-side state, offline-first experiences — React (or a similar framework) is still the right tool. They are different hammers; most teams that adopt htmx keep a framework for the one screen that truly needs it.
How do I debug htmx requests?
Open your browser's Network tab: every htmx request is a normal HTTP request, and you can inspect the URL, the parameters, and the exact HTML fragment the server returned. htmx also tags its requests with an HX-Request: true header, so your server can tell an htmx request apart from a full page load and return a fragment instead of a whole page.