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
| Header | Value | Description |
|---|---|---|
| HX-Request | "true" | Always present on HTMX requests; confirms the request came from HTMX |
| HX-Trigger | element id or name | The id (or name) of the element that triggered the request |
| HX-Trigger-Name | name attribute value | The name attribute of the triggering element |
| HX-Target | element id | The id of the target element |
| HX-Current-URL | URL string | The 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-Prompt | user input string | The 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.
