There’s a moment in every developer’s life when they realize they’re not building an app anymore—they’re building a build system to build an app. htmx is my loud, unapologetic answer to that feeling. It doesn’t “replace” JavaScript so much as it rejects the idea that every interactive UI needs a bespoke JavaScript universe. Instead: extend HTML, let the server do the heavy lifting, and ship.

This is not nostalgia. It’s pragmatism—especially if your app looks like most real apps: CRUD screens, searchable lists, dashboards, comment threads, account pages, and all the workflows that don’t require a game engine in the browser.

The problem isn’t JavaScript—it’s the complexity tax

Modern front-end tooling has trained us to believe that interactivity always comes with a specific stack shape: a client-side framework, a bundler, a state management library, a router, a data-fetch layer, a form abstraction, and then a pile of glue code to keep it all from falling apart.

You can see the pattern in how teams talk:

  • “We’re rewriting the state management.”
  • “We need a new data layer.”
  • “The UI is hard because the data is hard.”
  • “We’re fighting re-renders.”

To be clear: JavaScript is powerful. Frameworks are useful. But the default industry approach often charges an upfront complexity tax on every page, regardless of whether the page needs it.

Most CRUD apps don’t need client-side routing gymnastics. They don’t need a virtual DOM diffing strategy. They need predictable behavior, fast iteration, and a server that’s already great at generating HTML.

htmx asks the simple question we avoid: why are we replacing a perfectly good document with a second, synchronized document?

The htmx idea: make HTML interactive, not obsolete

htmx takes HTML seriously. It turns “static markup plus a small enhancement layer” into a full application style—without insisting you abandon HTML or introduce a virtual DOM.

The core move is this: use HTML attributes to declare behavior.

Instead of writing a component that fetches data, renders UI, and updates state, you write markup that says things like:

  • When this button is clicked, make a request.
  • When the response arrives, swap a portion of the page.
  • When the user types, debounce and query.
  • When a form is submitted, send it and replace the results.

That’s it. The “state” lives where it should for these apps: on the server, reflected in HTML.

A tiny example: delete with a confirmation and a swap

Imagine a list of items:

<div id="items">
  <div id="item-42">
    <span>Item 42</span>
    <button
      hx-delete="/items/42"
      hx-confirm="Delete this item?"
      hx-target="#item-42"
      hx-swap="outerHTML"
    >
      Delete
    </button>
  </div>
</div>

No client-side store. No reducer. No rehydration drama. Your delete endpoint returns what the UI should become—often 204 for “remove it,” or a small fragment to replace the row.

This is not magic; it’s mechanical sympathy. The browser remains the browser: it loads a page, you update parts of it, and the DOM stays the DOM.

“No build step” doesn’t mean “no discipline”

Let’s clear a common misconception: “no build step” doesn’t mean “no engineering.” You still need boundaries, good endpoints, and a clean mental model for what’s rendered where.

What htmx changes is where complexity belongs.

With a React-style approach, complexity migrates into JavaScript architecture: component trees, client caching rules, state synchronization, optimistic updates, and a thicket of abstractions. With htmx, the complexity shifts back toward server design:

  • Your endpoints become the UI’s “program.”
  • Your HTML fragments become the UI’s “render output.”
  • Your controllers decide what state exists and what the user sees next.

Practical advice: design endpoints that return partial HTML fragments with clear responsibility.

For example:

  • GET /items returns a full page for initial load.
  • GET /items/search?query=... returns a fragment for the results list.
  • POST /items returns the updated list fragment (or the created row).
  • DELETE /items/:id returns a fragment to remove or replace the row.

Your UI composition becomes a series of intentional swaps.

When htmx fits like a glove (and when it doesn’t)

Let’s be honest: htmx isn’t a universal replacement for all front-end development. It’s a different trade.

Great fits

htmx shines when:

  • The UI is mostly server-driven content.
  • CRUD flows dominate.
  • Users expect forms, tables, filtering, pagination, and actions that map cleanly to HTTP.
  • You can tolerate navigation that is mostly “page loads + partial updates.”

A job board. A blog admin. A ticketing system. A content CMS. An internal tool. Most of the software you or your company actually needs.

Not ideal (by default)

htmx will fight you if you need:

  • Highly interactive, real-time canvas-like experiences.
  • Complex client-side validation logic that depends on large local state.
  • Heavy offline-first behavior where the server is only a later reconciliation step.
  • Ultra-granular animations tied tightly to high-frequency UI state.

In those cases, you can still use htmx as a companion—enhancing the “boring parts” while reserving a dedicated front-end for the parts that truly need it.

My opinionated stance: start with server-rendered HTML and let interactivity grow only where it earns its complexity.

How the architecture changes your day-to-day

Here’s why I’m “joining” the htmx camp: it changes how development feels.

1) Debugging becomes straightforward

When something renders wrong, you can inspect the HTML you received. When an interaction fails, you can inspect the network request and the returned fragment.

No component introspection. No “why did the state update twice?” no phantom re-renders. You can reason about behavior using tools you already know: request/response, DOM updates, and HTML output.

2) Iteration loops get tighter

Design your app like you would design pages:

  • Create a route that renders a view.
  • Add a small fragment endpoint for updates.
  • Wire an attribute in the markup to swap it in.

The feedback cycle shrinks because you’re not constantly building an app shell just to test a small UI behavior.

3) Performance becomes natural

Server-rendered pages start with meaningful HTML. That reduces the “blank screen until hydration” problem that haunts many client-heavy architectures. Even when you use JavaScript sparingly, the baseline experience is already there.

And because updates are targeted swaps, you often avoid re-fetching and re-rendering entire application states for what should be local changes.

A pragmatic migration strategy: start small, win early

If you’re currently deep in a React-first setup, you don’t need to rip everything out to benefit from the underlying idea: fewer client-state responsibilities.

A migration strategy that works in real organizations:

  1. Pick one CRUD feature (e.g., “edit profile” or “manage tags”).
  2. Implement it server-rendered with standard HTML routes.
  3. Add htmx enhancements only where it improves flow (search-as-you-type, inline delete, partial refresh of a table).
  4. Keep the rest untouched until you’re confident you like the model.
  5. Measure engineering outcomes, not just UI metrics: time-to-change, regression frequency, and how often you fight state bugs.

The goal isn’t purity. It’s reducing the complexity tax. If htmx makes a single area of your product easier to maintain, that’s already a win.

Conclusion: the server isn’t “old”—it’s the solid foundation

htmx isn’t a rejection of modern web development; it’s a rejection of modern web development’s habits. It brings us back to a principle that scales: build reliable behavior around HTML and HTTP, and use the browser as a platform rather than as a rendering target for a separate UI model.

For the vast majority of apps—especially CRUD and content-driven workflows—htmx offers a simpler architecture that’s easier to understand, easier to debug, and faster to ship. I’m joining because it feels like building software again, not managing a framework’s ecosystem.