HTMX Request Headers

Every time HTMX sends a request to your server, it includes a set of extra HTTP headers that carry information about the request context — which element triggered it, what the current URL is, and whether the request came from HTMX at all. Your server reads these headers to decide how to respond: send a full page or a small fragment, redirect or update, and so on.

Why HTMX Sends Special Headers

A traditional browser request and an HTMX request look identical to the server from the outside — both are standard HTTP requests. The HTMX headers are the signal that tells the server: "This is not a full-page navigation. Send me just the HTML fragment I asked for."

  Regular browser GET (full navigation):
  GET /products HTTP/1.1
  Accept: text/html
  (no HTMX headers)
  → Server should return full page

  HTMX GET (partial update):
  GET /products HTTP/1.1
  Accept: text/html
  HX-Request: true
  HX-Target: main-content
  HX-Trigger: load-products-btn
  HX-Current-URL: https://example.com/dashboard
  → Server should return HTML fragment only

Standard HTMX Request Headers

HeaderValueDescription
HX-Request"true"Always present on HTMX requests; confirms the request came from HTMX
HX-Triggerelement id or nameThe id (or name) of the element that triggered the request
HX-Trigger-Namename attribute valueThe name attribute of the triggering element
HX-Targetelement idThe id of the target element
HX-Current-URLURL stringThe URL of the page making the request
HX-Boosted"true"Present when the request comes from an hx-boost enhanced link or form
HX-History-Restore-Request"true"Present when HTMX is restoring a page from browser history
HX-Promptuser input stringThe value entered in an hx-prompt dialog

Reading HTMX Headers on the Server

Flask (Python)

@app.route('/products')
def products():
    # Check if this is an HTMX request
    is_htmx = request.headers.get('HX-Request') == 'true'
    trigger_id = request.headers.get('HX-Trigger', '')
    target_id  = request.headers.get('HX-Target', '')

    if is_htmx:
        # Return only the product list fragment
        return render_template('partials/product_list.html')
    else:
        # Return the full page for direct browser navigation
        return render_template('products.html')

Node.js (Express)

app.get('/products', (req, res) => {
    const isHtmx = req.headers['hx-request'] === 'true';
    const trigger = req.headers['hx-trigger'];

    if (isHtmx) {
        return res.render('partials/product-list');
    }
    res.render('products');
});

The HX-Request Header: Full Page vs Fragment

The most useful pattern is using HX-Request to serve either a full page or a fragment from a single route. This keeps your URL structure clean — users can navigate directly to /products and get a full page, while HTMX requests to the same URL get only the fragment.

  Direct browser visit to /products:
  ┌────────────────────────────────────┐
  │  Header + Nav                      │
  │  ┌──────────────────────────────┐  │
  │  │  Product List                │  │  ← full page
  │  └──────────────────────────────┘  │
  │  Footer                            │
  └────────────────────────────────────┘

  HTMX request to /products:
  ┌──────────────────────────────┐
  │  Product List only           │  ← fragment only
  └──────────────────────────────┘

The HX-Trigger Header: Responding to Different Triggers

When multiple elements on a page can trigger the same URL, use HX-Trigger to identify which one fired and tailor the response:

@app.route('/load-content')
def load_content():
    trigger = request.headers.get('HX-Trigger')

    if trigger == 'news-btn':
        return render_template('partials/news.html')
    elif trigger == 'events-btn':
        return render_template('partials/events.html')
    else:
        return render_template('partials/default.html')
<button id="news-btn"   hx-get="/load-content" hx-target="#panel">News</button>
<button id="events-btn" hx-get="/load-content" hx-target="#panel">Events</button>
<div id="panel"></div>

Adding Custom Headers From the Client

You can add your own headers to any HTMX request using the htmx:configRequest event:

<script>
document.addEventListener('htmx:configRequest', function(event) {
    event.detail.headers['X-App-Version']   = '3.2.0';
    event.detail.headers['X-Session-Token'] = sessionStorage.getItem('token');
});
</script>

Or add headers to a specific element using hx-headers:

<button
  hx-post="/admin/action"
  hx-headers='{"X-Admin-Key": "abc123"}'
  hx-target="#result">
  Admin Action
</button>

The hx-prompt Header

The hx-prompt attribute shows a native browser text prompt and sends the user's input as the HX-Prompt header:

<button
  hx-delete="/account"
  hx-prompt="Type your username to confirm account deletion:"
  hx-target="#result">
  Delete Account
</button>
  Server reads the user's typed input:

  @app.route('/account', methods=['DELETE'])
  def delete_account():
      prompt_value = request.headers.get('HX-Prompt')
      current_user = get_current_user()
      if prompt_value != current_user.username:
          return '<p style="color:red">Username did not match.</p>', 422
      current_user.delete()
      return '<p>Account deleted.</p>'

Key Takeaway

HTMX adds a set of informational headers to every request it sends. The most important is HX-Request: true, which tells the server to respond with a fragment instead of a full page. Use HX-Trigger to differentiate between multiple elements sharing a route. Add custom headers globally with htmx:configRequest or per-element with hx-headers. Use hx-prompt to collect user input and receive it via the HX-Prompt header. These headers keep your URL structure clean while allowing the server to tailor its response precisely.

Leave a Comment

Your email address will not be published. Required fields are marked *