Skip to content
HTMX Laravel Tips and Tricks Laravel

Laravel HTMX Tutorial: From First Request to a Live Table

Raheel Shan

Raheel Shan

November 13th, 2026 · 7 min read

Laravel HTMX Tutorial: From First Request to a Live Table

This Laravel HTMX tutorial takes you from an empty page to a table that updates without a reload. HTMX adds attributes to HTML so a click or a keystroke can request a fragment from your server and swap it into the page. In Laravel that fragment is a Blade partial, so you keep your routes, validation and views and add almost no JavaScript. By the end you will have loaded a partial, submitted a form with the CSRF token, deleted a row, and handled validation errors, which is the step most tutorials skip.

Quick answer

  1. Add the HTMX script to your layout.
  2. Put hx-get and hx-target on an element.
  3. Return a Blade partial from the route it points at.
  4. Send the CSRF token with every non-GET request.
  5. Return a 200 response for validation errors, or HTMX will not swap them in.

Step 1: Add HTMX to your layout

You can load it from a CDN, with no build step:

<script src="https://unpkg.com/[email protected]"></script>

Or install it with npm install htmx.org and import it in resources/js/app.js. If you want to avoid a build step entirely, read I'm SO Looking Forward to Not Needing a Build System Again.

Step 2: Swap a partial into the page

Make a button that loads a list into a target element:

<button hx-get="/products/list" hx-target="#products" hx-swap="innerHTML">
    Load products
</button>

<div id="products"></div>

The route returns only the fragment:

Route::get('/products/list', function () {
    return view('products.partials.list', ['products' => Product::limit(10)->get()]);
});

hx-target is the element to update, and hx-swap says how: innerHTML replaces its contents, outerHTML replaces the element itself, beforeend appends. The full pattern, with one route for every partial, is in Laravel with HTMX: Use Blade Partials as Your API.

Step 3: Serve a full page or a partial from one route

HTMX adds an HX-Request: true header to its requests. You can use it to return the full page on a normal visit and only the fragment on an HTMX request:

public function index(Request $request)
{
    $products = Product::paginate(10);

    if ($request->header('HX-Request')) {
        return view('products.partials.table', compact('products'));
    }

    return view('products.index', compact('products'));
}

This keeps the URL working for people who open it directly and for search engines. Add a Vary: HX-Request response header if a cache sits in front of the route, so the two responses are not mixed.

Step 4: Send a form with CSRF

Laravel rejects POST, PUT and DELETE requests without a CSRF token. Set it once on the body, and HTMX sends it with every request:

<body hx-headers='{"X-CSRF-TOKEN": "{{ csrf_token() }}"}'>

Now a form can post and replace itself with the server's answer:

<form hx-post="/products" hx-target="#products" hx-swap="beforeend">
    <input type="text" name="name" required>
    <button type="submit">Add</button>
</form>
Route::post('/products', function (Request $request) {
    $data = $request->validate(['name' => 'required|max:100']);
    $product = Product::create($data);

    return view('products.partials.row', compact('product'));
});

Step 5: Delete a row

<tr id="product-{{ $product->id }}">
    <td>{{ $product->name }}</td>
    <td>
        <button hx-delete="/products/{{ $product->id }}"
                hx-target="#product-{{ $product->id }}"
                hx-swap="outerHTML"
                hx-confirm="Delete this product?">
            Delete
        </button>
    </td>
</tr>
Route::delete('/products/{product}', function (Product $product) {
    $product->delete();

    return response('', 200);
});

An empty 200 response with outerHTML removes the row. The hx-confirm attribute shows a browser confirmation first. For other ways to trigger a delete, see Four Ways to Trigger Delete Row from a Table.

The step most tutorials skip: validation errors

Laravel returns a 422 status when validation fails. By default HTMX does not swap 4xx or 5xx responses into the page, so the user sees nothing happen. You have two options:

  • Return the form again with the errors and a 200 status, for example return response()->view('products.partials.form', ['errors' => ...], 200). This is the simplest.
  • Tell HTMX to swap 422 responses. Add a listener once in your layout:
document.body.addEventListener('htmx:beforeSwap', function (event) {
    if (event.detail.xhr.status === 422) {
        event.detail.shouldSwap = true;
        event.detail.isError = false;
    }
});

With the listener in place, return the form partial with the errors from your validation handling and HTMX will show it.

Where to go next

FAQ

How do I use HTMX with Laravel? Add the HTMX script, put hx-get or hx-post on an element, and return a Blade partial from the route. Send the CSRF token in a header for non-GET requests.

Do I need to install anything for HTMX in Laravel? No package is required. A script tag is enough, or npm install htmx.org if you bundle your assets.

How do I pass the CSRF token with HTMX? Set hx-headers='{"X-CSRF-TOKEN": "..."}' on the body tag with csrf_token().

Why does HTMX not show my validation errors? Laravel returns 422, and HTMX does not swap 4xx responses by default. Return 200 with the form, or allow the swap in htmx:beforeSwap.

Is HTMX good for SEO? Yes, when the first page load is full server-rendered HTML, as in the example above. Content that only appears after a user action is not crawled.

Build the interactive parts without React

Live search, tables and forms on server-rendered HTML, with HTMX or jsRibbon and no build step.

Bring the page you want to make interactive and we build the first piece together in 60 minutes.

Book an HTMX and jsRibbon call

Found this useful?

Support the work that goes into these posts.

Support
Comments


Comment created and will be displayed once approved.