The hx-head extension lets htmx responses update the document’s <head> without a full page load.

Installing

<script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-beta6/dist/htmx.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/htmx.org@4.0.0-beta6/dist/ext/hx-head.min.js"></script>

Usage

Add Head Content

Use hx-head="append" to add styles, scripts, or metadata without removing the current <head>:

<button hx-get="/dark-theme" hx-target="#preview"> Preview Dark Theme </button> <div id="preview">Light theme</div>

The server responds with a <head> and <body>:

<html> <head hx-head="append"> <link rel="stylesheet" href="/dark-theme.css"> </head> <body> Dark theme enabled </body> </html>

htmx adds the stylesheet, then swaps the body content into #preview:

<head> <!-- Existing head content --> <link rel="stylesheet" href="/dark-theme.css"> </head> <body> <button hx-get="/dark-theme" hx-target="#preview"> Preview Dark Theme </button> <div id="preview">Dark theme enabled</div> </body>

Replace Head Content

Use hx-head="merge" when a response represents a new page:

<a hx-get="/settings" hx-target="body">Settings</a>

The current page contains:

<head> <title>Home</title> <link rel="stylesheet" href="/site.css"> <link rel="stylesheet" href="/home.css"> </head>

The server responds with the complete <head> for the settings page:

<html> <head hx-head="merge"> <title>Settings</title> <link rel="stylesheet" href="/site.css"> <link rel="stylesheet" href="/settings.css"> </head> <body> <h1>Settings</h1> </body> </html>

The merge keeps exact matches, adds new elements, and removes elements missing from the response:

<head> <title>Settings</title> <link rel="stylesheet" href="/site.css"> <link rel="stylesheet" href="/settings.css"> </head> <body> <h1>Settings</h1> </body>

Keep Existing Head Content

A merge removes current elements missing from the response. Add hx-preserve="true" to keep one:

<head> <script src="/analytics.js" hx-preserve="true"></script> </head>

The script remains even when the next response <head> omits it.

Attributes

hx-head

Set hx-head on a response <head> to choose how it updates the document head:

<head hx-head="<strategy>"> ... </head>

hx-head="merge"

Add response elements and remove current elements missing from the response:

<head hx-head="merge"> ... </head>

New elements are added at the end of the current <head>.

hx-head="append"

Add response elements without removing current elements:

<head hx-head="append"> ... </head>

New elements are added at the end of the current <head>.

hx-head="re-eval"

Set hx-head="re-eval" on a <head> child to replace and run an exact match again:

<head hx-head="merge"> <script hx-head="re-eval"> initializePage() </script> </head>

The response must include the element each time it should run.

Defaults

You can omit hx-head from the response. The request target chooses the strategy:

Request targetDefaultEffect
bodymergeMake the current <head> match the response <head>
Any other elementappendAdd response elements without removing current elements

Events

Event data is available on event.detail.

Element events expose:

event.detail.headElement // Element being added or removed

htmx:before:head:merge

Fires before the extension processes a response <head>.

document.addEventListener('htmx:before:head:merge', event => { if (!shouldUpdateHead(event.detail.ctx)) event.preventDefault() })

The event detail includes the request ctx. Cancel the event to skip all head processing for the response.

htmx:before:head:add

Fires before the extension adds a head element.

document.addEventListener('htmx:before:head:add', event => { if (event.detail.headElement.matches('script[data-untrusted]')) { event.preventDefault() } })

Cancel the event to skip that element.

htmx:before:head:remove

Fires before the extension removes a current <head> element.

document.addEventListener('htmx:before:head:remove', event => { if (event.detail.headElement.matches('[data-keep]')) { event.preventDefault() } })

Cancel the event to keep that element.

htmx:after:head:merge

Fires after the extension finishes updating the <head>.

event.detail = { added, // Added elements kept, // Exact matches left in place removed // Removed elements }
document.addEventListener('htmx:after:head:merge', event => { console.log('Head elements added:', event.detail.added.length) })

Notes

  • <style> applies and <script> runs before swap; <script defer> runs after:

    <head hx-head="append"> <!-- Applies before swap --> <style>...</style> <!-- Runs before swap --> <script src="/app.js"></script> <!-- Runs after swap --> <script src="/after.js" defer></script> </head>
  • Responses can contain <head> tags without a root <html> element:

    <!-- No <html> wrapper --> <head hx-head="append"> <meta name="theme-color" content="#000"> </head> <div>Saved</div>