Skip to main content

htmx in 2026: When Is Less JavaScript the Right Choice?

June 2, 2026

</>

The modern frontend default is a heavy JavaScript Single-Page Application: React, Next.js, a state management library, a bundler, hydration, and tens of kilobytes of JavaScript shipped before your user sees anything meaningful. For many applications, this is overkill.

htmx takes a radically different position: it extends HTML with new attributes that let the browser make AJAX requests, update parts of the DOM, and handle user interactions — without writing a single line of JavaScript. Instead of maintaining a client-side state machine, you simply render HTML on the server in response to every interaction.

In 2026, htmx is experiencing significant adoption among developers building server-centric applications with Python (Django, FastAPI), Ruby on Rails, Go, and even Node.js backends.


What htmx Does

htmx extends HTML with a small set of attributes:

<!-- Make this button trigger a POST request and replace the #result div -->
<button
  hx-post="/api/toggle-like"
  hx-target="#like-count"
  hx-swap="outerHTML"
  hx-include="[nam='post-id']"
>
  ♡ Like
</button>
<input type="hidden" name="post-id" value="123">
<span id="like-count">42 likes</span>

When clicked, htmx POSTs to /api/toggle-like, takes the HTML response, and replaces the #like-count element — no JavaScript written, no state to manage.

The server returns a fragment of HTML:

<!-- Server response for POST /api/toggle-like -->
<span id="like-count">43 likes</span>

That's it. The full interaction cycle.


Core htmx Attributes

AttributePurposeExample
hx-getTrigger GET requesthx-get="/search?q=..."
hx-postTrigger POST requesthx-post="/api/comment"
hx-targetCSS selector of element to updatehx-target="#results"
hx-swapHow to swap: innerHTML, outerHTML, beforeend, afterendhx-swap="beforeend"
hx-triggerWhat triggers the requesthx-trigger="keyup delay:300ms"
hx-includeInclude additional form inputshx-include="[name='csrf']"
hx-push-urlUpdate browser URL on requesthx-push-url="true"
hx-indicatorShow loading indicatorhx-indicator="#spinner"

Real Examples

<!-- Search that queries as you type, with 300ms debounce -->
<input
  type="search"
  name="q"
  placeholder="Search products..."
  hx-get="/api/products/search"
  hx-trigger="keyup delay:300ms changed"
  hx-target="#search-results"
  hx-indicator="#search-spinner"
>

<div id="search-spinner" class="htmx-indicator">
  <span>Searching...</span>
</div>

<div id="search-results">
  <!-- Server returns product card HTML fragments here -->
</div>

Server handler (Next.js):

// app/api/products/search/route.ts
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const q = searchParams.get('q') ?? '';

  const products = await db.query(
    'SELECT * FROM products WHERE name ILIKE $1 LIMIT 10',
    [`%${q}%`]
  );

  // Return HTML fragment, not JSON
  const html = products.rows.map(p => `
    <div clas="product-card">
      <h3>${p.name}</h3>
      <p>$${p.price}</p>
    </div>
  `).join('');

  return new Response(html, { headers: { 'Content-Type': 'text/html' } });
}

Infinite Scroll

<!-- Last item triggers loading the next page -->
<div class="post-list" id="posts">
  <!-- Posts render here -->

  <!-- The last item triggers the next page load -->
  <div
    hx-get="/api/posts?pag=2"
    hx-trigger="revealed"
    hx-swap="outerHTML"
    hx-target="this"
  >
    Loading more...
  </div>
</div>

Delete with Confirmation

<article id="post-42">
  <h2>My Blog Post</h2>
  <button
    hx-delete="/api/posts/42"
    hx-target="#post-42"
    hx-swap="outerHTML swap:0.5s"
    hx-confirm="Are you sure you want to delete this post?"
  >
    Delete
  </button>
</article>

The server responds with an empty string or a notification HTML fragment, and htmx removes the article element.


When htmx Is the Right Choice

htmx shines in specific contexts:

Server-rendered applications where the backend already generates HTML (Django, Rails, Laravel, Go templates, Next.js with RSC).

CRUD-heavy admin panels and dashboards where forms, tables, and modals are the primary interaction pattern.

Content-heavy sites (blogs, documentation, news) with some interactive enhancements.

Team skill constraints — backend developers who are less comfortable with React can build full interactive features.

Performance-critical applications — htmx ships as a single 14KB script with zero dependencies.


When htmx Is NOT the Right Choice

Highly stateful client interfaces — complex drag-and-drop, real-time collaborative editing, or applications with intricate client-side state graphs.

Offline-first applications — htmx requires a server response for every interaction.

Large teams with established React expertise — the productivity cost of switching outweighs the simplicity benefit.

Complex client-side data transformations — if your frontend logic involves significant data processing before display, a proper client-side framework handles this better.


htmx with Next.js

htmx works with any HTTP server, including Next.js:

// app/page.tsx — Server Component with htmx
import Script from 'next/script';

export default function Page() {
  return (
    <>
      <Script src="https://unpkg.com/htmx.org@2.0.0" />

      <form hx-post="/api/newsletter" hx-target="#form-response" hx-swap="innerHTML">
        <input type="email" name="email" placeholder="your@email.com" required />
        <button type="submit">Subscribe</button>
      </form>

      <div id="form-response"></div>
    </>
  );
}
// app/api/newsletter/route.ts — Returns HTML fragment
export async function POST(request: Request) {
  const formData = await request.formData();
  const email = formData.get('email') as string;

  await subscribeToNewsletter(email);

  return new Response(
    '<p clas="success">✓ Subscribed successfully!</p>',
    { headers: { 'Content-Type': 'text/html' } }
  );
}

Comparing Bundle Sizes

ApproachJavaScript ShippedInteraction Model
htmx~14KBServer-rendered HTML fragments
Alpine.js~16KBDeclarative JS in HTML attributes
Vanilla JSVariesManual DOM manipulation
React + Next.js~120KB+Client-side state + virtual DOM
Vue~90KB+Client-side reactive state

Conclusion

htmx is not a replacement for React for all use cases. It is a powerful tool for the substantial category of web applications that do not actually need client-side state management — applications where the server already knows everything and the UI is primarily a styled view of that server state. If your application fits this description, htmx gives you dynamic, interactive interfaces with dramatically less JavaScript, simpler architecture, and significantly better initial load performance. In 2026, reaching for a full SPA framework by default for every project is a choice worth questioning.

Recommended Posts