HTMX - The Simple Markup Extension PHP Developers Need

HTMX - The Simple Markup Extension PHP Developers Need

  1. PHP 🐘
  2. 2023-10-20 21:00
  3. 9 min read

The JavaScript ecosystem moves fast. Too fast. Every year brings a new framework, a new build tool, a new way to compose components. Developers spend more time configuring Webpack, Vite, and TypeScript than writing application logic.

HTMX offers a different path. It is a 14KB JavaScript library that lets you build interactive web applications using only HTML attributes. No build step. No virtual DOM. No component files. Just HTML that knows how to make HTTP requests and swap content in the DOM.

What You’ll Learn

  • How HTMX replaces JavaScript frameworks with HTML attributes
  • Building a live search component with HTMX and PHP
  • Form handling with PUT, DELETE, and PATCH requests
  • Performance optimization through targeted DOM swaps
  • Security considerations including CSRF and XSS prevention
  • Comparing HTMX against Livewire, Vue, and React
  • When HTMX is the right choice for your project

How HTMX Works

HTMX extends HTML with custom attributes that tell the browser to make HTTP requests and update the DOM with the response. Instead of writing JavaScript to fetch data and update the page, you add an attribute to an HTML element.

<button hx-post="/clicked" hx-swap="outerHTML">
    Click Me
</button>

When clicked, this button sends a POST request to /clicked and replaces itself with the HTML response. No JavaScript event listeners. No fetch API calls. No state management.

The core attributes are:

  • hx-get, hx-post, hx-put, hx-patch, hx-delete — HTTP methods for the request
  • hx-trigger — what triggers the request (click, change, keyup, etc.)
  • hx-target — which element receives the response
  • hx-swap — how the response replaces the target (innerHTML, outerHTML, beforeend, etc.)
  • hx-indicator — an element shown during the request

These attributes form a complete toolkit for building interactive interfaces without writing a single line of JavaScript.

Live Search with HTMX

A live search component demonstrates the pattern clearly. The input field sends a POST request on each keyup event and injects the results into a target container.

<h3>
    Search Contacts
    <span class="htmx-indicator">
        <img src="/img/bars.svg"/> Searching...
    </span>
</h3>
 
<input class="form-control"
       type="search"
       name="search"
       placeholder="Begin Typing To Search Users..."
       hx-post="/search"
       hx-trigger="keyup changed delay:500ms, search"
       hx-target="#search-results"
       hx-indicator=".htmx-indicator">
 
<table class="table">
    <thead>
        <tr>
            <th>First Name</th>
            <th>Last Name</th>
            <th>Email</th>
        </tr>
    </thead>
    <tbody id="search-results">
    </tbody>
</table>

The key attributes:

  • hx-post="/search" — sends the search term to the server
  • hx-trigger="keyup changed delay:500ms, search" — fires 500ms after the user stops typing
  • hx-target="#search-results" — injects the response into the table body
  • hx-indicator=".htmx-indicator" — shows a spinner during the request

On the PHP side, the /search endpoint receives the search term, queries the database, and returns HTML:

// search.php
$search = $_POST['search'] ?? '';
 
$results = $db->query(
    "SELECT first_name, last_name, email
     FROM contacts
     WHERE first_name LIKE ? OR last_name LIKE ?
     LIMIT 10",
    ["%{$search}%", "%{$search}%"]
);
 
foreach ($results as $contact) {
    echo "<tr>
        <td>{$contact['first_name']}</td>
        <td>{$contact['last_name']}</td>
        <td>{$contact['email']}</td>
    </tr>";
}

The server returns raw HTML, not JSON. HTMX inserts this HTML directly into the DOM. No client-side rendering, no template compilation, no hydration.

Form Handling

HTMX forms submit via AJAX without a page refresh. The form attributes specify the HTTP method, target, and swap strategy.

<form hx-put="/contact/1"
      hx-target="this"
      hx-swap="outerHTML">
    <div>
        <label>First Name</label>
        <input type="text" name="firstName" value="Joe">
    </div>
    <div>
        <label>Last Name</label>
        <input type="text" name="lastName" value="Blow">
    </div>
    <div>
        <label>Email Address</label>
        <input type="email" name="email"
               value="[email protected]">
    </div>
    <button class="btn">Submit</button>
    <button class="btn"
            hx-get="/contact/1">Cancel</button>
</form>

The form PUTs to /contact/1 and replaces itself with the server response on success. The cancel button GETs the original contact view, effectively reverting the form without JavaScript.

HTMX supports all HTTP methods: GET, POST, PUT, PATCH, and DELETE. This is crucial for RESTful applications where different operations require different methods.

Performance Through Targeted DOM Swaps

HTMX updates only the parts of the page that change. A “Load More” button fetches the next page of results and replaces itself:

<tr id="replaceMe">
    <td colspan="3">
        <button class='btn'
                hx-get="/contacts?page=2"
                hx-target="#replaceMe"
                hx-swap="outerHTML">
            Load More Agents...
            <img class="htmx-indicator"
                 src="/img/bars.svg">
        </button>
    </td>
</tr>

When the button is clicked, HTMX requests /contacts?page=2, and the server returns the next set of rows with a new “Load More” button that points to page 3. This pattern paginates without JavaScript, without page refreshes, and without loading content the user has not requested.

Security with HTMX

HTMX operates within the browser’s security model. It respects Content-Security-Policy headers and encourages developers to avoid inline event handlers like onclick.

<!-- Instead of onclick, use hx-on -->
<button hx-on="click: alert('You clicked me!')">
    Click Me
</button>
 
<!-- Or better, use declarative HTMX attributes -->
<button hx-post="/example"
        hx-on="htmx:configRequest:
               event.detail.parameters.example = 'Hello'">
    Post Me!
</button>

For CSRF protection, include the token in HTMX headers:

<meta name="csrf-token" content="{{ csrf_token() }}">
<script>
    document.body.addEventListener('htmx:configRequest', (e) => {
        e.detail.headers['X-CSRFToken'] =
            document.querySelector('meta[name="csrf-token"]')
                .content;
    });
</script>

Alternatively, set a global header:

<script>
    htmx.config.headers['X-CSRFToken'] = 'your-csrf-token';
</script>

HTMX vs. The Alternatives

HTMX vs. Livewire. Livewire is Laravel-specific and uses a component-based architecture. HTMX works with any backend. Livewire excels at form-heavy, interactive components. HTMX excels at augmenting existing HTML with AJAX behavior.

HTMX vs. AlpineJS. AlpineJS provides JavaScript-like reactivity with HTML attributes. HTMX focuses on server communication. They complement each other — Alpine for client-side interactivity, HTMX for server interactions.

HTMX vs. Vue/React. Vue and React are full SPA frameworks with state management, routing, and component systems. HTMX is a progressive enhancement library. Choose Vue or React when you need a rich client-side application with complex state. Choose HTMX when you want server-rendered HTML with targeted AJAX updates.

HTMX vs. Turbo. Turbo (from the Hotwire framework) is Rails-specific. HTMX is framework-agnostic. Turbo uses a similar philosophy but is tightly coupled to the Rails ecosystem.

HTMX Events and Lifecycle

HTMX fires a rich set of events throughout the request lifecycle, allowing you to hook into each phase:

  • htmx:beforeRequest — before the AJAX request is sent
  • htmx:afterRequest — after the response is received
  • htmx:beforeSwap — before the DOM is updated
  • htmx:afterSwap — after the DOM is updated
  • htmx:configRequest — modify the request before it is sent
document.body.addEventListener('htmx:beforeRequest', (e) => {
    console.log('Requesting:', e.detail.requestConfig.url);
});
 
document.body.addEventListener('htmx:afterRequest', (e) => {
    if (e.detail.failed) {
        console.error('Request failed:', e.detail.xhr.status);
    }
});

These events make HTMX extensible. You can add loading animations, error handling, analytics tracking, or custom headers without modifying the HTML attributes.

HTMX Swapping Strategies

The hx-swap attribute controls how the response replaces the target element. The options cover every reasonable use case:

  • innerHTML — Replace the target’s content (default)
  • outerHTML — Replace the target element itself
  • beforebegin — Insert before the target
  • afterbegin — Insert as the first child of the target
  • beforeend — Insert as the last child of the target
  • afterend — Insert after the target
  • delete — Delete the target element regardless of response
  • none — Do not swap (useful when combined with events)
<!-- Append new items to a list -->
<button hx-get="/more-items"
        hx-target="#item-list"
        hx-swap="beforeend">
    Load More
</button>
 
<!-- Replace the entire table with new data -->
<button hx-get="/refresh-table"
        hx-target="#data-table"
        hx-swap="outerHTML">
    Refresh
</button>
 
<!-- Delete an item and remove its row -->
<button hx-delete="/items/1"
        hx-target="closest tr"
        hx-swap="delete">
    Delete
</button>

The hx-swap-oob attribute extends this by allowing elements in the response to swap content outside the target. This enables updating multiple parts of the page with a single server response.

Real-World Use Cases

Dashboard Widgets. Each widget on a dashboard can independently refresh via HTMX without a full page reload. A stock ticker, weather widget, and activity feed each fetch their own data on their own schedule.

Inline Editing. Click a text field to transform it into an editable input. On blur, the form submits via PUT and the updated value replaces the input. This pattern works without JavaScript event handlers.

Infinite Scroll. Replace “Load More” buttons with automatic loading triggered by the revealed event when the element scrolls into view.

Dynamic Forms. Add and remove form fields without JavaScript. A “Add Another” button fetches a new field group from the server and appends it to the form.

Best Practices

Return HTML from your server endpoints. HTMX expects HTML responses. Do not return JSON. Keep a single source of truth in your server-side templates.

Use hx-trigger wisely. Debounce search inputs with delay:500ms. Trigger on revealed for infinite scroll. Use intersect for lazy loading images and sections.

Combine with AlpineJS for client-side interactivity. Use HTMX for server communication and AlpineJS for animations, toggles, and local state.

Leverage hx-indicator for user feedback. Show loading spinners during requests. This is a small detail with a large impact on perceived performance.

Common Mistakes to Avoid

Returning JSON instead of HTML. HTMX swaps HTML fragments. JSON requires additional JavaScript to render. Embrace server-rendered HTML.

Ignoring error states. Server errors should return appropriate HTTP status codes and error HTML. HTMX swaps error responses just like success responses.

Over-fetching. HTMX loads what you request. Be mindful of the HTML size returned by your endpoints. Keep responses focused on what the client needs.

Skipping CSRF protection. HTMX requests are normal HTTP requests. Protect them with CSRF tokens just like standard form submissions.

Frequently Asked Questions

Does HTMX work with Laravel’s Blade? Perfectly. Blade components render HTML that HTMX swaps. Use Blade’s @csrf directive in forms and pass CSRF tokens through HTMX headers.

Can I use HTMX with React components? Technically yes, but it defeats the purpose. HTMX replaces the need for a client-side framework. If you already use React, stick with React’s patterns.

Does HTMX support WebSockets? Yes. The hx-ws attribute establishes WebSocket connections for real-time updates. HTMX handles reconnection automatically.

Is HTMX production-ready? Yes. HTMX powers production applications across industries. Its small size, simple API, and focus on hypermedia make it a reliable choice.

How does HTMX handle browser history? The hx-push-url attribute pushes a new URL into the browser history when a request completes. The hx-replace-url attribute replaces the current URL without adding history.

Conclusion

HTMX represents a return to the fundamentals of the web. HTML was designed for hypermedia — documents that link to and include other documents. HTMX restores this vision by making every HTML element capable of making HTTP requests and updating the page with the response.

For PHP developers, HTMX is particularly natural. Your existing Blade templates, Twig templates, or plain PHP files already know how to render HTML. HTMX connects that HTML to user interactions without an intermediate JSON layer.

Start small. Add HTMX to one form or one search input. Experience the relief of removing a JavaScript build step. Then expand to more components as you recognize how many interactive patterns are just HTTP requests with HTML responses.

PHP HTMX HTML AJAX Hypermedia Livewire AlpineJS Full-Stack