NOTE: If you don't know what htmx is, you should check out htmx.org first!
Before you get started, be sure to include the htmx cdn at the top of your page:
<script src='https://unpkg.com/htmx.org@2.0.4'></script>
<button hx-get="/htmx/time">
Click to get server time
</button>
This is the most basic htmx example. We set up a route called /htmx/time that returns the current server time. Then, we have a button that can be clicked to load the route with hx-get.
It assumes a default trigger action of "click", and a default target to place the html return from the get into whichever div was clicked.
<div hx-get="/htmx/time">
Click to get server time
</div>
This is the exact same example as above, but we show that *any* element can be used as a hypermedia control. In this case, we are using a <div> instead of a <button>.
It assumes a default trigger action of "click", and a default target to place the html return from the get into whichever div was clicked.
<div hx-get="/htmx/time"
hx-trigger="click"
hx-target="this"
hx-swap="innerHTML">
Click to get server time
</div>
This does exactly the same thing as the examples above. But here we explicitly set the hx-trigger, the hx-target, and the hx-swap method to the exact same as the defaults.
This is just to illustrate that htmx assumes certain default values. In this case, the hx-get on a div assumes a default trigger of click, a default target of this (the same div that is clicked), and a default swap method of innerHTML.
The next several examples will demonstrate what happens when you change these defaults.
<div hx-get="/htmx/time"
hx-target="#hx-get-target">
Click this to update target
</div>
<div id="hx-get-target"></div>
We have now changed the hx-target so that we can send the html returned from hx-get to any element on the page. Use a css selector (in this case the id #hx-get-target) to set the target.
In this case, we set a small div under the clickable div, so when you click it, the target is filled with the html from the /htmx/time route.
<div hx-get="/htmx/time"
hx-trigger="mouseenter">
Hover over this to get time
</div>
We have now changed the hx-trigger so that we can activate it on a different user event.
In this case, we are using mouseenter to let the user trigger the hx-get when they hover over the div.
<div hx-get="/htmx/time"
hx-swap="outerHTML">
Click to replace with the server time
</div>
We have now changed the hx-swap away from the default of innerHTML to outerHTML.
This means that when we get our response, the html completely replaces the element (the div we clicked in this case) with the html from the server.
<div hx-get="/htmx/time"
hx-target="#some-notification-target">
Click this to update notifications
</div>
Again, we are using a css selector to set the target, but now we are targeting the element #some-notification-target, which is outside of our original example.
You can see when you click to load the server time, it is now placed in the notification target at the top right of the page.
<div hx-get="/htmx/form">
Click to load an html form
</div>
WARNING: DOESN'T WORK!
Click to load, then try to enter data in the form to see what's wrong.
The reason the form doesn't work is because by default it is loaded into the innerHTML of the div you clicked. The problem is that the div you clicked then wraps the form, and the hx-get attribute on that div will continue to work as expected, and reload the form!
<div hx-get="/htmx/form"
hx-target="#hx-get-form-target">
Click to load an html form
</div>
<div id="hx-get-form-target"></div>
Now we should be able to load the form AND use it correctly.
The reason this works is because we are now loading the form to a separate element than the one that has the click trigger on it.
You can click the div again to *reload* the blank form from the server.
<div hx-get="/htmx/form"
hx-swap="outerHTML">
Click to load an html form
</div>
Again, we should be able to load the form AND use it correctly now.
The reason it works this time is that we have changed the hx-swap default from innerHTML (the default) to outerHTML.
hx-swap="outerHTML" will replace the entire target itself, rather than replace the content inside the div as the innerHTML default does.
<div hx-get="/htmx/time"
hx-trigger="click">
Click me to load html
</div>
Put description here
<div hx-get="/htmx/time"
hx-trigger="click once">
This will load just once
</div>
Loads when input changed
<div id="select-target">
Load the html when the input is changed
</div>
<select hx-get="/htmx/time"
hx-trigger="change"
hx-target="#select-target">
<option value="">None</option>
<option value="1">One</option>
<option value="2">Two</option>
</select>
Loads html when the select changes.
<div hx-get="/htmx/time"
hx-trigger="revealed">
When this gets to the to right place on the page, it will be replaced.
</div>
Loads html when the select changes.
<div hx-get="/htmx/time"
hx-trigger="mouseenter">
This will be replaced when the mouse goes over it
</div>
Loads html when the select changes.
<div hx-get="/htmx/time"
hx-trigger="mouseleave">
This will be replaced when the mouse leaves the div
</div>
Mouse leave
<input hx-get="/htmx/time"
hx-trigger="keyup"
hx-target="#input-keyup-div" />
<div id="input-keyup-div">
This will get replaced
</div>
When key pressed
<div hx-get="/htmx/time"
hx-trigger="load">
You will never see this.
</div>
Now, we are changing the default hx-trigger so that instead of "click", it will trigger the hx-get on "load".
The "load" trigger option is triggered when the page loads. This pattern is commonly called lazy loading. Because it happens immediately on load, you will likely not see the original text in the div.
<div hx-get="/htmx/time"
hx-trigger="every 5s">
Polling every 5 seconds
</div>
Put description here
<button hx-get="/htmx/time"
hx-target="#time-target-click">
Click me
</button>
<div id="time-target-click">
Replace this target on click
</div>
<button hx-get="/htmx/time"
hx-target=".time-target-click">
Click me
</button>
<div class="time-target-click">
Replace this target on click
</div>
<div>
Replace this target on click
<button hx-get="/htmx/time"
hx-target="closest div">
Click me
</button>
</div>
<button hx-get="/htmx/time"
hx-target="#swap-target">
Click to replace html
</button>
<div id="swap-target">
This will be replaced
</div>
<button hx-get="/htmx/time"
hx-target="#swap-target"
hx-swap="outerHTML">
Click to replace html
</button>
<div id="swap-target">
This will be replaced
</div>
<button hx-get="/htmx/time"
hx-target="#swap-target-list"
hx-swap="beforebegin">
Click to place the new html before the list
</button>
<ul id="swap-target-list" style="border: 2px solid gray; padding: 5px;">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
<button hx-get="/htmx/time"
hx-target="#swap-target-list-after"
hx-swap="afterbegin">
Click to place the new html at the top of the list
</button>
<ul id="swap-target-list-after" style="border: 2px solid gray; padding: 5px;">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
<button hx-get="/htmx/time"
hx-target="#swap-target-list-bottom"
hx-swap="beforeend">
Click to place the new html at the bottom of the list
</button>
<ul id="swap-target-list-bottom" style="border: 2px solid gray; padding: 5px;">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
<button hx-get="/htmx/time"
hx-target="#swap-target-list-under"
hx-swap="afterend">
Click to place the new html underneath the list
</button>
<ul id="swap-target-list-under" style="border: 2px solid gray; padding: 5px;">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
<button hx-get="/htmx/time"
hx-target="#swap-target-list-delete"
hx-swap="delete">
Click to delete the list after the html loads
</button>
<ul id="swap-target-list-delete" style="border: 2px solid gray; padding: 5px;">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
<button hx-get="/htmx/time"
hx-target="#swap-target-list-none"
hx-swap="none">
Click to not change the the list after the html loads
</button>
<ul id="swap-target-list-none" style="border: 2px solid gray; padding: 5px;">
<li>Item 1</li>
<li>Item 2</li>
<li>Item 3</li>
</ul>
<style>
dialog {
display: flex;
opacity: 0;
pointer-events: none;
transform: scale(0.9);
}
dialog[open] {
display: flex;
opacity: 1;
transform: scale(1);
pointer-events: inherit;
transition: opacity 0.1s ease-in,
transform 0.1s ease-in;
}
dialog::backdrop {
background: rgba(0,0,0,0.3);
backdrop-filter: blur(3px);
}
</style>
<button hx-get="/examples/modal-form"
hx-trigger="mouseenter"
hx-target="#modal-content"
onclick="window.my_modal.showModal()">
Open Modal
</button>
<dialog id="my_modal"
class="rounded-lg shadow-lg w-3/4">
<div class="w-full">
<div class="font-bold text-black text-lg border-b p-4">
<div class="float-right cursor-pointer text-gray-400 hover:text-gray-600 transition"
onclick="window.my_modal.close()">
<svg xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="size-6">
<path stroke-linecap="round"
stroke-linejoin="round"d="M6 18 18 6M6 6l12 12" />
</svg>
</div>
Dynamic Modal Example
</div>
<div id="modal-content">
<div class="p-8 text-center text-xl text-gray-400">
Loading....
</div>
</div>
</div>
</dialog>
HTMX extensions add additional functionality to the core library. Extensions are loaded via CDN or npm and enabled using the hx-ext attribute. They can enhance HTMX with features like head tag support, client-side templates, and more.
To use the head-support extension, first include the script and enable it on your page:
<script src="https://cdn.jsdelivr.net/npm/htmx-ext-head-support@2.0.2"></script>
<body hx-ext="head-support">
The server responses contain complete HTML documents with updated titles:
<html>
<head>
<title hx-head="re-eval">🟢 Active - hype cp | Hypermedia Copy & Paste</title>
</head>
</html>
<html>
<head>
<title hx-head="re-eval">🔴 Inactive - hype cp | Hypermedia Copy & Paste</title>
</head>
</html>
<button hx-get="/htmx/head-support/title-active"
hx-swap="none">
Set Active Title
</button>
<button hx-get="/htmx/head-support/title-inactive"
hx-swap="none">
Set Inactive Title
</button>
The head-support extension allows HTMX to process <head> tags in responses, enabling dynamic updates to the page title, meta tags, CSS, and other head elements.
In this example, clicking the buttons sends requests that return only a <head> tag with a new <title>. The hx-head="re-eval" attribute on the title tag tells the extension to replace the existing title with the new one.
Using hx-swap="none" prevents any content changes while the head-support extension processes the <head> tag and updates only the page title.
<button hx-get="/htmx/head-support/css-blue"
hx-swap="none">
Blue Theme
</button>
<button hx-get="/htmx/head-support/css-red"
hx-swap="none">
Red Theme
</button>
<div id="css-demo" class="p-4 border rounded">
<h3>CSS Demo</h3>
<p>Click the buttons above to change the head tag for the CSS of this box.</p>
</div>
Click the buttons above to change the head tag for the CSS of this box.
This example demonstrates how to dynamically update CSS styles using the head-support extension.
The responses contain <style> tags with hx-head="re-eval" that replace existing styles with the same ID. The extension processes the <head> tag and updates the page's CSS.
Using hx-swap="none" prevents any content changes while only the styling gets updated.
The server responses contain style tags with the same ID for replacement:
<html>
<head>
<style id="demo-styles" hx-head="re-eval">
#css-demo {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: 3px solid #4c51bf;
box-shadow: 0 10px 25px rgba(102, 126, 234, 0.3);
}
#css-demo h3 {
color: #e6fffa;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
</style>
</head>
</html>
<html>
<head>
<style id="demo-styles" hx-head="re-eval">
#css-demo {
background: linear-gradient(135deg, #f093fb 0%, #f5576c 100%);
color: white;
border: 3px solid #e53e3e;
box-shadow: 0 10px 25px rgba(245, 87, 108, 0.3);
}
#css-demo h3 {
color: #fed7d7;
text-shadow: 2px 2px 4px rgba(0,0,0,0.3);
}
</style>
</head>
</html>
The preload extension allows you to load HTML fragments into your browser's cache before they are requested by the user, making pages appear to load nearly instantaneously. It's enabled by default on this page.
<button hx-get="/htmx/preload/content"
hx-target="#preload-demo"
preload>
Hover and click me (content preloads on mousedown)
</button>
<div id="preload-demo" class="p-4 border rounded mt-4">
<p class="text-gray-500">Content will load here...</p>
</div>
Content will load here...
The preload extension begins loading content when the user starts clicking (mousedown event). This gives your server a 100-200ms head start, making the response appear nearly instantaneous.
Simply add the preload attribute to any hx-get element or <a href> link. The extension automatically handles the preloading in the background.
Note: Preloading only works with GET requests. POST, PUT, and DELETE requests cannot be preloaded for security reasons.
<form hx-get="/htmx/preload/form-response"
hx-target="#form-demo"
preload>
<div class="mb-4">
<label class="block text-sm font-medium mb-2">Select Plan:</label>
<input type="radio" name="plan" value="basic" class="mr-2"> Basic
<input type="radio" name="plan" value="pro" class="mr-2"> Pro
<input type="radio" name="plan" value="enterprise" class="mr-2"> Enterprise
</div>
<div class="mb-4">
<label class="block text-sm font-medium mb-2">Features:</label>
<input type="checkbox" name="features[]" value="video" class="mr-2"> Video
<input type="checkbox" name="features[]" value="audio" class="mr-2"> Audio
<input type="checkbox" name="features[]" value="support" class="mr-2"> Support
</div>
<div class="mb-4">
<label class="block text-sm font-medium mb-2">Region:</label>
<select name="region" class="border rounded px-3 py-2">
<option value="us">United States</option>
<option value="eu">Europe</option>
<option value="asia">Asia</option>
</select>
</div>
<button type="submit" class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
Submit Form (Content Preloaded!)
</button>
</form>
<div id="form-demo" class="p-4 border rounded mt-4">
<p class="text-gray-500">Form responses will appear here...</p>
</div>
Form responses will appear here...
The preload extension can preload form responses when users interact with form elements like radio buttons, checkboxes, and select dropdowns.
When you hover over or interact with form elements, the extension preloads the server response as if the form was submitted with those values. This makes form interactions appear nearly instantaneous.
Try it: Hover over different radio buttons, checkboxes, or change the select dropdown to see responses preload in the background.
The idiomorph extension provides DOM morphing capabilities, allowing HTMX to smoothly transition between different states by reusing existing DOM nodes when possible. This creates much smoother animations and preserves element state.
<button hx-get="/htmx/idiomorph/simple"
hx-target="#simple-morph"
hx-swap="morph">
Load Simple Content
</button>
<div id="simple-morph">
<p>Click the button to see simple morphing in action!</p>
</div>
Click the button to see simple morphing in action!
This simple example demonstrates basic DOM morphing. The button loads new content using hx-swap="morph", which smoothly transitions the entire element.
<button hx-get="/htmx/idiomorph/content-1"
hx-target="#morph-demo"
hx-swap="morph:innerHTML">
Morph Swap 1
</button>
<button hx-get="/htmx/idiomorph/content-2"
hx-target="#morph-demo"
hx-swap="innerHTML">
Regular Swap 2
</button>
<button hx-get="/htmx/idiomorph/content-3"
hx-target="#morph-demo"
hx-swap="morph:innerHTML">
Morph Swap 3
</button>
<div id="morph-demo" class="p-6 border-2 border-purple-200 rounded-lg mt-4 bg-purple-50">
<h3 class="text-purple-800 font-semibold mb-2">Initial Content</h3>
<p class="text-purple-700">This is the starting content. Click the buttons above to see smooth DOM morphing in action!</p>
<div class="mt-3 p-2 bg-purple-100 rounded">
<span class="text-sm text-purple-600">Element with preserved state</span>
</div>
</div>
This is the starting content. Click the buttons above to see smooth DOM morphing in action!
The idiomorph extension uses DOM morphing to smoothly transition between different HTML states. Instead of completely replacing elements, it intelligently reuses existing DOM nodes when possible.
Using hx-swap="morph:innerHTML" tells HTMX to morph only the inner children of the target element, leaving the container itself untouched. Compare this with hx-swap="innerHTML" which completely replaces the content.
Try it: Click "Morph Swap" buttons to see smooth transitions, then click "Regular Swap 2" to see the difference with traditional swapping. Notice how morphing preserves element state while regular swapping creates a jarring replacement.
The response-targets extension allows you to specify different target elements for different HTTP response codes. This enables better error handling and user experience by routing responses to appropriate UI locations.
To use the response-targets extension, first include the script and enable it on your page:
<script src="https://cdn.jsdelivr.net/npm/htmx-ext-response-targets@2.0.2"></script>
<div hx-ext="response-targets">
<div hx-ext="response-targets">
<div id="response-div" class="p-4 border rounded bg-gray-50">
<p class="text-gray-600">Response will appear here...</p>
</div>
<button hx-get="/htmx/response-targets/register"
hx-target="#response-div"
hx-target-5*="#serious-errors"
hx-target-404="#not-found"
class="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600">
Register!
</button>
<div id="serious-errors" class="p-4 border rounded bg-red-50 mt-4">
<p class="text-red-600">Server errors will appear here...</p>
</div>
<div id="not-found" class="p-4 border rounded bg-yellow-50 mt-4">
<p class="text-red-600">404 errors will appear here...</p>
</div>
</div>
Response will appear here...
Server errors will appear here...
404 errors will appear here...
The response-targets extension allows you to route different HTTP response codes to different UI elements. In this example:
Random Response Demo: The server randomly returns different status codes to demonstrate the extension in action:
Click the button multiple times to see how different responses automatically get routed to their appropriate targets.
<div hx-ext="response-targets">
<div id="response-div-2" class="p-4 border rounded bg-gray-50">
<p class="text-gray-600">Response will appear here...</p>
</div>
<button hx-get="/htmx/response-targets/register"
hx-target="#response-div-2"
hx-target-error="#any-errors"
class="bg-green-500 text-white px-4 py-2 rounded hover:bg-green-600">
Register (All Errors)
</button>
<div id="any-errors" class="p-4 border rounded bg-red-50 mt-4">
<p class="text-red-600">All 4xx and 5xx errors will appear here...</p>
</div>
</div>
Response will appear here...
All 4xx and 5xx errors will appear here...
Instead of handling different error codes separately, you can use hx-target-error to route all 4xx and 5xx responses to a single error target.
This is useful when you want to display all errors in a unified error area rather than having separate locations for different types of errors.
<div hx-ext="response-targets">
<div id="response-div-3" class="p-4 border rounded bg-gray-50">
<p class="text-gray-600">Response will appear here...</p>
</div>
<button hx-get="/htmx/response-targets/not-found"
hx-target="#response-div-3"
hx-target-404="#not-found-2"
class="bg-purple-500 text-white px-4 py-2 rounded hover:bg-purple-600">
Test 404 Response
</button>
<div id="not-found-2" class="p-4 border rounded bg-yellow-50 mt-4">
<p class="text-yellow-600">404 errors will appear here...</p>
</div>
</div>
Response will appear here...
404 errors will appear here...
This example specifically tests 404 response targeting. The button makes a request to a route that returns a 404 status, demonstrating how the extension routes error responses to the appropriate target.
Note: The hx-target-404 attribute will catch 404 responses and route them to the specified target, while successful responses (200) go to the default hx-target.