htmx Examples





Click to Edit

This pattern starts with a UI that shows the details of a contact. The div has a button that will get the editing UI for the contact from /contact/1/edit <div hx-target="this" hx-swap="outerHTML"> <div><label>First Name</label>: Joe</div> <div><label>Last Name</label>: Blow</div> <div><label>Email</label>: joe@blow.com</div> <button hx-get="/contact/1/edit" class="btn primary"> Click To Edit </button> </div> This returns a form that can be used to edit the contact <form hx-put="/contact/1" hx-target="this" hx-swap="outerHTML"> <div> <label>First Name</label> <input type="text" name="firstName" value="Joe"> </div> <div class="form-group"> <label>Last Name</label> <input type="text" name="lastName" value="Blow"> </div> <div class="form-group"> <label>Email Address</label> <input type="email" name="email" value="joe@blow.com"> </div> <button class="btn">Submit</button> <button class="btn" hx-get="/contact/1">Cancel</button> </form> The form issues a PUT back to /contact/1, following the usual REST-ful pattern.

Bulk Update

accomplished by putting a form around a table, with checkboxes in the table, and then including the checked values in the form submission (POST request): <form id="checked-contacts" hx-post="/users" hx-swap="outerHTML settle:3s" hx-target="#toast"> <table> <thead> <tr> <th>Name</th> <th>Email</th> <th>Active</th> </tr> </thead> <tbody id="tbody"> <tr> <td>Joe Smith</td> <td>joe@smith.org</td> <td><input type="checkbox" name="active:joe@smith.org"></td> </tr> ... </tbody> </table> <input type="submit" value="Bulk Update" class="btn primary"> <span id="toast"></span> </form> The server will bulk-update the statuses based on the values of the checkboxes. We respond with a small toast message about the update to inform the user, and use ARIA to politely announce the update for accessibility. #toast.htmx-settling { opacity: 100; } #toast { background: #E1F0DA; opacity: 0; transition: opacity 3s ease-out; } The cool thing is that, because HTML form inputs already manage their own state, we don’t need to re-render any part of the users table. The active users are already checked and the inactive ones unchecked! You can see a working example of this code below.

Click to Load

the final row: <tr id="replaceMe"> <td colspan="3"> <button class='btn primary' hx-get="/contacts/?page=2" hx-target="#replaceMe" hx-swap="outerHTML"> Load More Agents... <img class="htmx-indicator" src="/img/bars.svg"> </button> </td> </tr> This row contains a button that will replace the entire row with the next page of results (which will contain a button to load the next page of results). And so on.

Delete Row

table body: <table class="table delete-row-example"> <thead> <tr> <th>Name</th> <th>Email</th> <th>Status</th> <th></th> </tr> </thead> <tbody hx-confirm="Are you sure?" hx-target="closest tr" hx-swap="outerHTML swap:1s"> ... </tbody> </table> The table body has a hx-confirm attribute to confirm the delete action. It also set the target to be the closest tr that is, the closest table row, for all the buttons (hx-target is inherited from parents in the DOM.) The swap specification in hx-swap says to swap the entire target out and to wait 1 second after receiving a response. This last bit is so that we can use the following CSS: tr.htmx-swapping td { opacity: 0; transition: opacity 1s ease-out; } To fade the row out before it is swapped/removed. Each row has a button with a hx-delete attribute containing the url on which to issue a DELETE request to delete the row from the server. This request responds with a 200 status code and empty content, indicating that the row should be replaced with nothing. <tr> <td>Angie MacDowell</td> <td>angie@macdowell.org</td> <td>Active</td> <td> <button class="btn danger" hx-delete="/contact/1"> Delete </button> </td> </tr>

Edit Row

<table class="table delete-row-example"> <thead> <tr> <th>Name</th> <th>Email</th> <th></th> </tr> </thead> <tbody hx-target="closest tr" hx-swap="outerHTML"> ... </tbody> </table> This will tell the requests from within the table to target the closest enclosing row that the request is triggered on and to replace the entire row. Here is the HTML for a row: <tr> <td>${contact.name}</td> <td>${contact.email}</td> <td> <button class="btn danger" hx-get="/contact/${contact.id}/edit" hx-trigger="edit" onClick="let editing = document.querySelector('.editing') if(editing) { Swal.fire({title: 'Already Editing', showCancelButton: true, confirmButtonText: 'Yep, Edit This Row!', text:'Hey! You are already editing a row! Do you want to cancel that edit and continue?'}) .then((result) => { if(result.isConfirmed) { htmx.trigger(editing, 'cancel') htmx.trigger(this, 'edit') } }) } else { htmx.trigger(this, 'edit') }"> Edit </button> </td> </tr> Here we are getting a bit fancy and only allowing one row at a time to be edited, using some JavaScript. We check to see if there is a row with the .editing class on it and confirm that the user wants to edit this row and dismiss the other one. If so, we send a cancel event to the other row so it will issue a request to go back to its initial state. We then trigger the edit event on the current element, which triggers the htmx request to get the editable version of the row. Note that if you didn’t care if a user was editing multiple rows, you could omit the hyperscript and custom hx-trigger, and just let the normal click handling work with htmx. You could also implement mutual exclusivity by simply targeting the entire table when the Edit button was clicked. Here we wanted to show how to integrate htmx and JavaScript to solve the problem and narrow down the server interactions a bit, plus we get to use a nice SweetAlert confirm dialog. Finally, here is what the row looks like when the data is being edited: <tr hx-trigger='cancel' class='editing' hx-get="/contact/${contact.id}"> <td><input name='name' value='${contact.name}'></td> <td><input name='email' value='${contact.email}'></td> <td> <button class="btn danger" hx-get="/contact/${contact.id}"> Cancel </button> <button class="btn danger" hx-put="/contact/${contact.id}" hx-include="closest tr"> Save </button> </td> </tr> Here we have a few things going on: First off the row itself can respond to the cancel event, which will bring back the read-only version of the row. There is a cancel button that allows cancelling the current edit. Finally, there is a save button that issues a PUT to update the contact. Note that there is an hx-include that includes all the inputs in the closest row. Tables rows are notoriously difficult to use with forms due to HTML constraints (you can’t put a form directly inside a tr) so this makes things a bit nicer to deal with.

Lazy Loading

state that looks like this: <div hx-get="/graph" hx-trigger="load"> <img alt="Result loading..." class="htmx-indicator" width="150" src="/img/bars.svg"/> </div> Which shows a progress indicator as we are loading the graph. The graph is then loaded and faded gently into view via a settling CSS transition: .htmx-settling img { opacity: 0; } img { transition: opacity 300ms ease-in; }

Inline Validation

we need to create a form with an input that POSTs back to the server with the value to be validated and updates the DOM with the validation results. We start with this form: <h3>Signup Form</h3> <form hx-post="/contact"> <div hx-target="this" hx-swap="outerHTML"> <label>Email Address</label> <input name="email" hx-post="/contact/email" hx-indicator="#ind"> <img id="ind" src="/img/bars.svg" class="htmx-indicator"/> </div> <div class="form-group"> <label>First Name</label> <input type="text" class="form-control" name="firstName"> </div> <div class="form-group"> <label>Last Name</label> <input type="text" class="form-control" name="lastName"> </div> <button class="btn primary">Submit</button> </form> Note that the first div in the form has set itself as the target of the request and specified the outerHTML swap strategy, so it will be replaced entirely by the response. The input then specifies that it will POST to /contact/email for validation, when the changed event occurs (this is the default for inputs). It also specifies an indicator for the request. When a request occurs, it will return a partial to replace the outer div. It might look like this: <div hx-target="this" hx-swap="outerHTML" class="error"> <label>Email Address</label> <input name="email" hx-post="/contact/email" hx-indicator="#ind" value="test@foo.com"> <img id="ind" src="/img/bars.svg" class="htmx-indicator"/> <div class='error-message'>That email is already taken. Please enter another email.</div> </div> Note that this div is annotated with the error class and includes an error message element. This form can be lightly styled with this CSS: .error-message { color:red; } .error input { box-shadow: 0 0 3px #CC0000; } .valid input { box-shadow: 0 0 3px #36cc00; } To give better visual feedback. Below is a working demo of this example. The only email that will be accepted is test@test.com.

Infinite Scroll

Let’s focus on the final row (or the last element of your content): <tr hx-get="/contacts/?page=2" hx-trigger="revealed" hx-swap="afterend"> <td>Agent Smith</td> <td>void29@null.org</td> <td>55F49448C0</td> </tr> This last element contains a listener which, when scrolled into view, will trigger a request. The result is then appended after it. The last element of the results will itself contain the listener to load the next page of results, and so on.
revealed - triggered when an element is scrolled into the viewport (also useful for lazy-loading). If you are using overflow in css like overflow-y: scroll you should use intersect once instead of revealed.

Active Search

We start with a search input and an empty table: <h3> Search Contacts <span class="htmx-indicator"> <img src="/img/bars.svg"/> Searching... </span> </h3> <input class="form-control" type="search" name="search" placeholder="Begin Typing To Search Users..." hx-post="/search" hx-trigger="input changed delay:500ms, search" hx-target="#search-results" hx-indicator=".htmx-indicator"> <table class="table"> <thead> <tr> <th>First Name</th> <th>Last Name</th> <th>Email</th> </tr> </thead> <tbody id="search-results"> </tbody> </table> The input issues a POST to /search on the input event and sets the body of the table to be the resulting content. Note that the keyup event could be used as well, but would not fire if the user pasted text with their mouse (or any other non-keyboard method). We add the delay:500ms modifier to the trigger to delay sending the query until the user stops typing. Additionally, we add the changed modifier to the trigger to ensure we don’t send new queries when the user doesn’t change the value of the input (e.g. they hit an arrow key, or pasted the same value). Since we use a search type input we will get an x in the input field to clear the input. To make this trigger a new POST we have to specify another trigger. We specify another trigger by using a comma to separate them. The search trigger will be run when the field is cleared but it also makes it possible to override the 500 ms input event delay by just pressing enter. Finally, we show an indicator when the search is in flight with the hx-indicator attribute.

Progress Bar

We start with an initial state with a button that issues a POST to /start to begin the job: <div hx-target="this" hx-swap="outerHTML"> <h3>Start Progress</h3> <button class="btn primary" hx-post="/start"> Start Job </button> </div> This div is then replaced with a new div containing status and a progress bar that reloads itself every 600ms: <div hx-trigger="done" hx-get="/job" hx-swap="outerHTML" hx-target="this"> <h3 role="status" id="pblabel" tabindex="-1" autofocus>Running</h3> <div hx-get="/job/progress" hx-trigger="every 600ms" hx-target="this" hx-swap="innerHTML"> <div class="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="0" aria-labelledby="pblabel"> <div id="pb" class="progress-bar" style="width:0%"> </div> </div> </div> This progress bar is updated every 600 milliseconds, with the “width” style attribute and aria-valuenow attributed set to current progress value. Because there is an id on the progress bar div, htmx will smoothly transition between requests by settling the style attribute into its new value. This, when coupled with CSS transitions, makes the visual transition continuous rather than jumpy. Finally, when the process is complete, a server returns HX-Trigger: done header, which triggers an update of the UI to “Complete” state with a restart button added to the UI (we are using the class-tools extension in this example to add fade-in effect on the button): <div hx-trigger="done" hx-get="/job" hx-swap="outerHTML" hx-target="this"> <h3 role="status" id="pblabel" tabindex="-1" autofocus>Complete</h3> <div hx-get="/job/progress" hx-trigger="none" hx-target="this" hx-swap="innerHTML"> <div class="progress" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="122" aria-labelledby="pblabel"> <div id="pb" class="progress-bar" style="width:122%"> </div> </div> </div> <button id="restart-btn" class="btn primary" hx-post="/start" classes="add show:600ms"> Restart Job </button> </div> This example uses styling cribbed from the bootstrap progress bar: .progress { height: 20px; margin-bottom: 20px; overflow: hidden; background-color: #f5f5f5; border-radius: 4px; box-shadow: inset 0 1px 2px rgba(0,0,0,.1); } .progress-bar { float: left; width: 0%; height: 100%; font-size: 12px; line-height: 20px; color: #fff; text-align: center; background-color: #337ab7; -webkit-box-shadow: inset 0 -1px 0 rgba(0,0,0,.15); box-shadow: inset 0 -1px 0 rgba(0,0,0,.15); -webkit-transition: width .6s ease; -o-transition: width .6s ease; transition: width .6s ease; }

Cascading Selects

To begin we start with a default value for the make select: Audi. We render the model select for this make. We then have the make select trigger a GET to /models to retrieve the models options and target the models select. Here is the code: <div> <label >Make</label> <select name="make" hx-get="/models" hx-target="#models" hx-indicator=".htmx-indicator"> <option value="audi">Audi</option> <option value="toyota">Toyota</option> <option value="bmw">BMW</option> </select> </div> <div> <label>Model</label> <select id="models" name="model"> <option value="a1">A1</option> ... </select> <img class="htmx-indicator" width="20" src="/img/bars.svg"> </div> When a request is made to the /models end point, we return the models for that make: <option value='325i'>325i</option> <option value='325ix'>325ix</option> <option value='X5'>X5</option> And they become available in the model select.

Animations

to add smooth animations and transitions to your web page using only CSS and HTML. Below are a few examples of various animation techniques. htmx also allows you to use the new View Transitions API for creating animations.

Basic CSS Animations

Color Throb

The simplest animation technique in htmx is to keep the id of an element stable across a content swap. If the id of an element is kept stable, htmx will swap it in such a way that CSS transitions can be written between the old version of the element and the new one. Consider this div: <style> .smooth { transition: all 1s ease-in; } </style> <div id="color-demo" class="smooth" style="color:red" hx-get="/colors" hx-swap="outerHTML" hx-trigger="every 1s"> Color Swap Demo </div> This div will poll every second and will get replaced with new content which changes the color style to a new value (e.g. blue): <div id="color-demo" class="smooth" style="color:blue" hx-get="/colors" hx-swap="outerHTML" hx-trigger="every 1s"> Color Swap Demo </div> Because the div has a stable id, color-demo, htmx will structure the swap such that a CSS transition, defined on the .smooth class, applies to the style update from red to blue, and smoothly transitions between them.

 Swap Transitions

Fade Out On Swap

If you want to fade out an element that is going to be removed when the request ends, you want to take advantage of the htmx-swapping class with some CSS and extend the swap phase to be long enough for your animation to complete. This can be done like so: <style> .fade-me-out.htmx-swapping { opacity: 0; transition: opacity 1s ease-out; } </style> <button class="fade-me-out" hx-delete="/fade_out_demo" hx-swap="outerHTML swap:1s"> Fade Me Out </button>

 Settling Transitions

Fade In On Addition

Building on the last example, we can fade in the new content by using the htmx-added class during the settle phase. You can also write CSS transitions against the target, rather than the new content, by using the htmx-settling class. <style> #fade-me-in.htmx-added { opacity: 0; } #fade-me-in { opacity: 1; transition: opacity 1s ease-out; } </style> <button id="fade-me-in" class="btn primary" hx-post="/fade_in_demo" hx-swap="outerHTML settle:1s"> Fade Me In </button>

 Request In Flight Animation

You can also take advantage of the htmx-request class, which is applied to the element that triggers a request. Below is a form that on submit will change its look to indicate that a request is being processed: <style> form.htmx-request { opacity: .5; transition: opacity 300ms linear; } </style> <form hx-post="/name" hx-swap="outerHTML"> <label>Name:</label><input name="name"><br/> <button class="btn primary">Submit</button> </form>

 Using the htmx class-tools Extension

Many interesting animations can be created by using the class-tools extension. Here is an example that toggles the opacity of a div. Note that we set the toggle time to be a bit longer than the transition time. This avoids flickering that can happen if the transition is interrupted by a class change. <style> .demo.faded { opacity:.3; } .demo { opacity:1; transition: opacity ease-in 900ms; } </style> <div class="demo" classes="toggle faded:1s">Toggle Demo</div>

Using the View Transition API

htmx provides access to the new View Transitions API via the transition option of the hx-swap attribute. Below is an example of a swap that uses a view transition. The transition is tied to the outer div via a view-transition-name property in CSS, and that transition is defined in terms of ::view-transition-old and ::view-transition-new, using @keyframes to define the animation. (Fuller details on the View Transition API can be found on the Chrome Developer Page on them.) The old content of this transition should slide out to the left and the new content should slide in from the right. Note that, as of this writing, the visual transition will only occur on Chrome 111+, but more browsers are expected to implement this feature in the near future. <style> @keyframes fade-in { from { opacity: 0; } } @keyframes fade-out { to { opacity: 0; } } @keyframes slide-from-right { from { transform: translateX(90px); } } @keyframes slide-to-left { to { transform: translateX(-90px); } } .slide-it { view-transition-name: slide-it; } ::view-transition-old(slide-it) { animation: 180ms cubic-bezier(0.4, 0, 1, 1) both fade-out, 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-to-left; } ::view-transition-new(slide-it) { animation: 420ms cubic-bezier(0, 0, 0.2, 1) 90ms both fade-in, 600ms cubic-bezier(0.4, 0, 0.2, 1) both slide-from-right; } </style> <div class="slide-it"> <h1>Initial Content</h1> <button class="btn primary" hx-get="/new-content" hx-swap="innerHTML transition:true" hx-target="closest div"> Swap It! </button> </div>

File Upload

with a progress bar. We will show two different implementation, one in pure javascript (using some utility methods in htmx) and one in hyperscript First the pure javascript version. We have a form of type multipart/form-data so that the file will be properly encoded We post the form to /upload We have a progress element We listen for the htmx:xhr:progress event and update the value attribute of the progress bar based on the loaded and total properties in the event detail. <form id='form' hx-encoding='multipart/form-data' hx-post='/upload'> <input type='file' name='file'> <button> Upload </button> <progress id='progress' value='0' max='100'></progress> </form> <script> htmx.on('#form', 'htmx:xhr:progress', function(evt) { htmx.find('#progress').setAttribute('value', evt.detail.loaded/evt.detail.total * 100) }); </script> The Hyperscript version is very similar, except: The script is embedded directly on the form element Hyperscript offers nicer syntax (although the htmx API is pretty nice too!) <form hx-encoding='multipart/form-data' hx-post='/upload' _='on htmx:xhr:progress(loaded, total) set #progress.value to (loaded/total)*100'> <input type='file' name='file'> <button> Upload </button> <progress id='progress' value='0' max='100'></progress> </form> Note that hyperscript allows you to destructure properties from details directly into variables

Preserving File Inputs after Form Errors

To overcome the problem of losing file input value in simple cases, you can adopt the following approach: Before: <form method="POST" id="binaryForm" enctype="multipart/form-data" hx-swap="outerHTML" hx-target="#binaryForm"> <input type="file" name="binaryFile"> <button type="submit">Submit</button> </form> After: <input form="binaryForm" type="file" name="binaryFile"> <form method="POST" id="binaryForm" enctype="multipart/form-data" hx-swap="outerHTML" hx-target="#binaryForm"> <button type="submit">Submit</button> </form>
    Form Restructuring: Move the binary file input outside the main form element in the HTML structure. Using the form Attribute: Enhance the binary file input by adding the form attribute and setting its value to the ID of the main form. This linkage associates the binary file input with the form, even when it resides outside the form element.

Dialogs

<div> <button class="btn primary" hx-post="/submit" hx-prompt="Enter a string" hx-confirm="Are you sure?" hx-target="#response"> Prompt Submission </button> <div id="response"></div> </div> The value provided by the user to the prompt dialog is sent to the server in a HX-Prompt header. In this case, the server simply echos the user input back. User entered <i>${response}</i>

Modal Dialogs with UIKit

This example shows how to use HTMX to display dynamic dialog using UIKit, and how to trigger its animation styles with little or no Javascript. We start with a button that triggers the dialog, along with a DIV at the bottom of your markup where the dialog will be loaded: This is an example of using HTMX to remotely load modal dialogs using UIKit. In this example we will use Hyperscript to demonstrate how cleanly that scripting language allows you to glue htmx and other libraries together. <button id="showButton" hx-get="/uikit-modal.html" hx-target="#modals-here" class="uk-button uk-button-primary" _="on htmx:afterOnLoad wait 10ms then add .uk-open to #modal">Open Modal</button> <div id="modals-here"></div> This button uses a GET request to /uikit-modal.html when this button is clicked. The contents of this file will be added to the DOM underneath the #modals-here DIV. Rather than using the standard UIKit Javascript library we are using a bit of Hyperscript, which triggers UIKit’s smooth animations. It is delayed by 10ms so that UIKit’s animations will run correctly. Finally, the server responds with a slightly modified version of UIKit’s standard modal <div id="modal" class="uk-modal" style="display:block;"> <div class="uk-modal-dialog uk-modal-body"> <h2 class="uk-modal-title">Modal Dialog</h2> <p>This modal dialog was loaded dynamically by HTMX.</p> <form _="on submit take .uk-open from #modal"> <div class="uk-margin"> <input class="uk-input" placeholder="What is Your Name?"> </div> <button type="button" class="uk-button uk-button-primary">Save Changes</button> <button id="cancelButton" type="button" class="uk-button uk-button-default" _="on click take .uk-open from #modal wait 200ms then remove #modal">Close</button> </form> </div> </div> Hyperscript on the button and the form trigger animations when this dialog is completed or canceled. If you didn’t use this Hyperscript, the modals will still work but UIKit’s fade in animations will not be triggered. You can, of course, use JavaScript rather than Hyperscript for this work, it’s just a lot more code: // This triggers the fade-in animation when a modal dialog is loaded and displayed window.document.getElementById("showButton").addEventListener("htmx:afterOnLoad", function() { setTimeout(function(){ window.document.getElementById("modal").classList.add("uk-open") }, 10) }) // This triggers the fade-out animation when the modal is closed. window.document.getElementById("cancelButton").addEventListener("click", function() { window.document.getElementById("modal").classList.remove("uk-open") setTimeout(function(){ window.document.getElementById("modals-here").innerHTML = "" ,200 }) })

Modal Dialogs in Bootstrap

This example shows how to use HTMX alongside original JavaScript provided by Bootstrap. We start with a button that triggers the dialog, along with a DIV at the bottom of your markup where the dialog will be loaded: <button hx-get="/modal" hx-target="#modals-here" hx-trigger="click" data-bs-toggle="modal" data-bs-target="#modals-here" class="btn primary">Open Modal</button> <div id="modals-here" class="modal modal-blur fade" style="display: none" aria-hidden="false" tabindex="-1"> <div class="modal-dialog modal-lg modal-dialog-centered" role="document"> <div class="modal-content"></div> </div> </div> This button uses a GET request to /modal when this button is clicked. The contents of this file will be added to the DOM underneath the #modals-here DIV. The server responds with a slightly modified version of Bootstrap’s standard modal <div class="modal-dialog modal-dialog-centered"> <div class="modal-content"> <div class="modal-header"> <h5 class="modal-title">Modal title</h5> </div> <div class="modal-body"> <p>Modal body text goes here.</p> </div> <div class="modal-footer"> <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button> </div> </div> </div>

Custom Modal Dialogs

it easy to build modal dialogs from scratch. Here is a quick example of one way to build them. Click here to see a demo of the final result:

 High Level Plan

We’re going to make a button that loads remote content from the server, then displays it in a modal dialog. The modal content will be added to the end of the <body> element, in a div named #modal. In this demo we’ll define some nice animations in CSS, and then use some Hyperscript to remove the modals from the DOM when the user is done. Hyperscript is not required with htmx, but the two were designed to be used together and it is much nicer for writing async & event oriented code than JavaScript, which is why we chose it for this example.

 Main Page HTML

<button class="btn primary" hx-get="/modal" hx-target="body" hx-swap="beforeend">Open a Modal</button>

 Modal HTML Fragment

<div id="modal" _="on closeModal add .closing then wait for animationend then remove me"> <div class="modal-underlay" _="on click trigger closeModal"></div> <div class="modal-content"> <h1>Modal Dialog</h1> This is the modal content. You can put anything here, like text, or a form, or an image. <br> <br> <button class="btn danger" _="on click trigger closeModal">Close</button> </div> </div>

 Custom Stylesheet

/***** MODAL DIALOG ****/ #modal { /* Underlay covers entire screen. */ position: fixed; top:0px; bottom: 0px; left:0px; right:0px; background-color:rgba(0,0,0,0.5); z-index:1000; /* Flexbox centers the .modal-content vertically and horizontally */ display:flex; flex-direction:column; align-items:center; /* Animate when opening */ animation-name: fadeIn; animation-duration:150ms; animation-timing-function: ease; } #modal > .modal-underlay { /* underlay takes up the entire viewport. This is only required if you want to click to dismiss the popup */ position: absolute; z-index: -1; top:0px; bottom:0px; left: 0px; right: 0px; } #modal > .modal-content { /* Position visible dialog near the top of the window */ margin-top:10vh; /* Sizing for visible dialog */ width:80%; max-width:600px; /* Display properties for visible dialog*/ border:solid 1px #999; border-radius:8px; box-shadow: 0px 0px 20px 0px rgba(0,0,0,0.3); background-color:white; padding:20px; /* Animate when opening */ animation-name:zoomIn; animation-duration:150ms; animation-timing-function: ease; } #modal.closing { /* Animate when closing */ animation-name: fadeOut; animation-duration:150ms; animation-timing-function: ease; } #modal.closing > .modal-content { /* Animate when closing */ animation-name: zoomOut; animation-duration:150ms; animation-timing-function: ease; } @keyframes fadeIn { 0% {opacity: 0;} 100% {opacity: 1;} } @keyframes fadeOut { 0% {opacity: 1;} 100% {opacity: 0;} } @keyframes zoomIn { 0% {transform: scale(0.9);} 100% {transform: scale(1);} } @keyframes zoomOut { 0% {transform: scale(1);} 100% {transform: scale(0.9);} }

Tabs (Using HATEOAS)

 Example Code (Main Page)

The main page simply includes the following HTML to load the initial tab into the DOM. <div id="tabs" hx-get="/tab1" hx-trigger="load delay:100ms" hx-target="#tabs" hx-swap="innerHTML"></div>

 Example Code (Each Tab)

Subsequent tab pages display all tabs and highlight the selected one accordingly. <div class="tab-list" role="tablist"> <button hx-get="/tab1" class="selected" role="tab" aria-selected="true" aria-controls="tab-content">Tab 1</button> <button hx-get="/tab2" role="tab" aria-selected="false" aria-controls="tab-content">Tab 2</button> <button hx-get="/tab3" role="tab" aria-selected="false" aria-controls="tab-content">Tab 3</button> </div> <div id="tab-content" role="tabpanel" class="tab-content"> Commodo normcore truffaut VHS duis gluten-free keffiyeh iPhone taxidermy godard ramps anim pour-over. Pitchfork vegan mollit umami quinoa aute aliquip kinfolk eiusmod live-edge cardigan ipsum locavore. Polaroid duis occaecat narwhal small batch food truck. PBR&B venmo shaman small batch you probably haven't heard of them hot chicken readymade. Enim tousled cliche woke, typewriter single-origin coffee hella culpa. Art party readymade 90's, asymmetrical hell of fingerstache ipsum. </div>

Tabs (Using JavaScript)

some duplication by offloading some of the work of re-rendering the tab HTML from your application server to your clients’ browsers. You may also consider a more idiomatic approach that follows the principle of Hypertext As The Engine Of Application State.

 Example Code

The HTML below displays a list of tabs, with added HTMX to dynamically load each tab pane from the server. A simple JavaScript event handler uses the take function to switch the selected tab when the content is swapped into the DOM. <div id="tabs" hx-target="#tab-contents" role="tablist" hx-on:htmx-after-on-load="let currentTab = document.querySelector('[aria-selected=true]'); currentTab.setAttribute('aria-selected', 'false') currentTab.classList.remove('selected') let newTab = event.target newTab.setAttribute('aria-selected', 'true') newTab.classList.add('selected')"> <button role="tab" aria-controls="tab-contents" aria-selected="true" hx-get="/tab1" class="selected">Tab 1</button> <button role="tab" aria-controls="tab-contents" aria-selected="false" hx-get="/tab2">Tab 2</button> <button role="tab" aria-controls="tab-contents" aria-selected="false" hx-get="/tab3">Tab 3</button> </div> <div id="tab-contents" role="tabpanel" hx-get="/tab1" hx-trigger="load"></div>

Keyboard Shortcuts

We start with a simple button that loads some content from the server: <button class="btn primary" hx-trigger="click, keyup[altKey&&shiftKey&&key=='D'] from:body" hx-post="/doit">Do It! (alt-shift-D)</button> Note that the button responds to both the click event (as usual) and also the keyup event when alt-shift-D is pressed. The from: modifier is used to listen for the keyup event on the body element, thus making it a “global” keyboard shortcut. You can trigger the demo below by either clicking on the button, or by hitting alt-shift-D. You can find out the conditions needed for a given keyboard shortcut here: https://javascript.info/keyboard-events

Sortable

javascript library with htmx. To begin we initialize the .sortable class with the Sortable javascript library: htmx.onLoad(function(content) { var sortables = content.querySelectorAll(".sortable"); for (var i = 0; i < sortables.length; i++) { var sortable = sortables[i]; var sortableInstance = new Sortable(sortable, { animation: 150, ghostClass: 'blue-background-class', // Make the `.htmx-indicator` unsortable filter: ".htmx-indicator", onMove: function (evt) { return evt.related.className.indexOf('htmx-indicator') === -1; }, // Disable sorting on the `end` event onEnd: function (evt) { this.option("disabled", true); } }); // Re-enable sorting on the `htmx:afterSwap` event sortable.addEventListener("htmx:afterSwap", function() { sortableInstance.option("disabled", false); }); } }) Next, we create a form that has some sortable divs within it, and we trigger an ajax request on the end event, fired by Sortable.js: <form class="sortable" hx-post="/items" hx-trigger="end"> <div class="htmx-indicator">Updating...</div> <div><input type='hidden' name='item' value='1'/>Item 1</div> <div><input type='hidden' name='item' value='2'/>Item 2</div> <div><input type='hidden' name='item' value='3'/>Item 3</div> <div><input type='hidden' name='item' value='4'/>Item 4</div> <div><input type='hidden' name='item' value='5'/>Item 5</div> </form> Note that each div has a hidden input inside of it that specifies the item id for that row. When the list is reordered via the Sortable.js drag-and-drop, the end event will be fired. htmx will then post the item ids in the new order to /items, to be persisted by the server. That’s it!

Updating Other Content

“I need to update other content on the screen. How do I do this?”
There are multiple ways to do so, and in this example will walk you through some of them. We’ll use the following basic UI to discuss this concept: a simple table of contacts, and a form below it to add new contacts to the table using hx-post. <h2>Contacts</h2> <table class="table"> <thead> <tr> <th>Name</th> <th>Email</th> <th></th> </tr> </thead> <tbody id="contacts-table"> ... </tbody> </table> <h2>Add A Contact</h2> <form hx-post="/contacts"> <label> Name <input name="name" type="text"> </label> <label> Email <input name="email" type="email"> </label> </form> The problem here is that when you submit a new contact in the form, you want the contact table above to refresh and include the contact that was just added by the form. What solutions to we have?

 Solution 1: Expand the Target

The easiest solution here is to “expand the target” of the form to enclose both the table and the form. For example, you could wrap the whole thing in a div and then target that div in the form: <div id="table-and-form"> <h2>Contacts</h2> <table class="table"> <thead> <tr> <th>Name</th> <th>Email</th> <th></th> </tr> </thead> <tbody id="contacts-table"> ... </tbody> </table> <h2>Add A Contact</h2> <form hx-post="/contacts" hx-target="#table-and-form"> <label> Name <input name="name" type="text"> </label> <label> Email <input name="email" type="email"> </label> </form> </div> Note that we are targeting the enclosing div using the hx-target attribute. You would need to render both the table and the form in the response to the POST to /contacts. This is a simple and reliable approach, although it might not feel particularly elegant.

 Solution 2: Out of Band Responses

A more sophisticated approach to this problem would use out of band swaps to swap in updated content to the DOM. Using this approach, the HTML doesn’t need to change from the original setup at all: <h2>Contacts</h2> <table class="table"> <thead> <tr> <th>Name</th> <th>Email</th> <th></th> </tr> </thead> <tbody id="contacts-table"> ... </tbody> </table> <h2>Add A Contact</h2> <form hx-post="/contacts"> <label> Name <input name="name" type="text"> </label> <label> Email <input name="email" type="email"> </label> </form> Instead of modifying something on the front end, in your response to the POST to /contacts you would include some additional content: <tbody hx-swap-oob="beforeend:#contacts-table"> <tr> <td>Joe Smith</td> <td>joe@smith.com</td> </tr> </tbody> <label> Name <input name="name" type="text"> </label> <label> Email <input name="email" type="email"> </label> This content uses the hx-swap-oob attribute to append itself to the #contacts-table, updating the table after a contact is added successfully.

 Solution 3: Triggering Events

An even more sophisticated approach would be to trigger a client side event when a successful contact is created and then listen for that event on the table, causing the table to refresh. <h2>Contacts</h2> <table class="table"> <thead> <tr> <th>Name</th> <th>Email</th> <th></th> </tr> </thead> <tbody id="contacts-table" hx-get="/contacts/table" hx-trigger="newContact from:body"> ... </tbody> </table> <h2>Add A Contact</h2> <form hx-post="/contacts"> <label> Name <input name="name" type="text"> </label> <label> Email <input name="email" type="email"> </label> </form> We have added a new end-point /contacts/table that re-renders the contacts table. Our trigger for this request is a custom event, newContact. We listen for this event on the body because when it is triggered by the response to the form, it will end up hitting the body due to event bubbling. When a successful contact creation occurs during a POST to /contacts, the response includes an HX-Trigger response header that looks like this: HX-Trigger:newContact This will trigger the table to issue a GET to /contacts/table and this will render the newly added contact row
(in addition to the rest of the table.) Very clean, event driven programming!

 Solution 4: Using the Path Dependencies Extension

A final approach is to use REST-ful path dependencies to refresh the table. Intercooler.js, the predecessor to htmx, had path-based dependencies integrated into the library. htmx dropped this as a core feature, but supports an extension, path deps, that gives you similar functionality. Updating our example to use the extension would involve loading the extension javascript and then annotating our HTML like so: <h2>Contacts</h2> <table class="table"> <thead> <tr> <th>Name</th> <th>Email</th> <th></th> </tr> </thead> <tbody id="contacts-table" hx-get="/contacts/table" hx-ext="path-deps" hx-trigger="path-deps" path-deps="/contacts"> ... </tbody> </table> <h2>Add A Contact</h2> <form hx-post="/contacts"> <label> Name <input name="name" type="text"> </label> <label> Email <input name="email" type="email"> </label> </form> Now, when the form posts to the /contacts URL, the path-deps extension will detect that and trigger an path-deps event on the contacts table, therefore triggering a request. The advantage here is that you don’t need to do anything fancy with response headers. The downside is that a request will be issued on every POST, even if a contact was not successfully created.

 Which should I use?

Generally I would recommend the first approach, expanding your target, especially if the elements that need to be updated are reasonably close to one another in the DOM. It is simple and reliable. After that, I would say it is a tossup between the custom event and an OOB swap approaches. I would lean towards the custom event approach because I like event-oriented systems, but that’s a personal preference. Which one you choose should be dictated by your own software engineering tastes and which of the two matches up better with your server side technology of choice. Finally, the path-deps approach is interesting, and if it fits well with your mental model and overall system architecture, it can be a fun way to avoid explicit refreshing. I would look at it last, however, unless the concept really grabs you.

A Customized Confirmation UI

action. This uses the default confirm() function in javascript which, while trusty, may not be consistent with your applications UX. In this example we will see how to use sweetalert2 to implement a custom confirmation dialog. Below are two examples, one using a click+custom event method, and one using the built-in hx-confirm attribute and the htmx:confirm event.

 Using on click+custom event

<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <button hx-get="/confirmed" hx-trigger='confirmed' onClick="Swal.fire({title: 'Confirm', text:'Do you want to continue?'}).then((result)=>{ if(result.isConfirmed){ htmx.trigger(this, 'confirmed'); } })"> Click Me </button> Here we use javascript to show a Sweet Alert 2 on a click, asking for confirmation. If the user confirms the dialog, we then trigger the request by triggering the custom “confirmed” event which is then picked up by hx-trigger.

 Vanilla JS, hx-confirm

<script src="https://cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script> document.addEventListener("htmx:confirm", function(e) { e.preventDefault() Swal.fire({ title: "Proceed?", text: `I ask you... ${e.detail.question}` }).then(function(result) { if(result.isConfirmed) e.detail.issueRequest(true) // use true to skip window.confirm }) }) </script> <button hx-get="/confirmed" hx-confirm="Some confirm text here"> Click Me </button> We add some javascript to invoke Sweet Alert 2 on a click, asking for confirmation. If the user confirms the dialog, we trigger the request by calling the issueRequest method. We pass skipConfirmation=true as argument to skip window.confirm. This allows to use hx-confirm’s value in the prompt which is convenient when the question depends on the element e.g. a django list: {% for client in clients %} <button hx-post="/delete/{{client.pk}}" hx-confirm="Delete {{client.name}}??">Delete</button> {% endfor %}

Async Authentication

The technique we will use here will take advantage of the fact that you can delay requests using the htmx:confirm event. We first have a button that should not issue a request until an auth token has been retrieved: <button hx-post="/example" hx-target="next output"> An htmx-Powered button </button> <output> -- </output> Next we will add some scripting to work with an auth promise (returned by a library): <script> // auth is a promise returned by our authentication system // await the auth token and store it somewhere let authToken = null; auth.then((token) => { authToken = token }) // gate htmx requests on the auth token htmx.on("htmx:confirm", (e)=> { // if there is no auth token if(authToken == null) { // stop the regular request from being issued e.preventDefault() // only issue it once the auth promise has resolved auth.then(() => e.detail.issueRequest()) } }) // add the auth token to the request as a header htmx.on("htmx:configRequest", (e)=> { e.detail.headers["AUTH"] = authToken }) </script> Here we use a global variable, but you could use localStorage or whatever preferred mechanism you want to communicate the authentication token to the htmx:configRequest event. With this code in place, htmx will not issue requests until the auth promise has been resolved.

Web Components

By default, HTMX doesn’t know anything about your web components, and won’t see anything inside their shadow DOM. Because of this, you’ll need to manually tell HTMX about your component’s shadow DOM by using htmx.process. customElements.define('my-component', class MyComponent extends HTMLElement { // This method runs when your custom element is added to the page connectedCallback() { const root = this.attachShadow({ mode: 'closed' }) root.innerHTML = ` <button hx-get="/my-component-clicked" hx-target="next div">Click me!</button> <div></div> ` htmx.process(root) // Tell HTMX about this component's shadow DOM } }) Once you’ve told HTMX about your component’s shadow DOM, most things should work as expected. However, note that selectors such as in hx-target will only see elements inside the same shadow DOM - if you need to access things outside of your web components, you can use one of the following options: host: Selects the element hosting the current shadow DOM global: If used as a prefix, selects from the main document instead of the current shadow DOM The same principles generally apply to web components that don’t use shadow DOM as well; while selectors won’t be encapsulated like with shadow DOM, you’ll still have to point HTMX to your component’s content by calling htmx.process.