Recommended resources
The freely available resources this lesson will direct you to.
Hello! In our last lesson, we explored how to use Out-of-Band (OOB) swaps to update multiple parts of the page from a single server response. This gave us the power to create more dynamic and responsive user interfaces, decoupling the action from a single, restrictive target.
However, as we build more of our application's logic into these AJAX-driven interactions, we risk breaking a fundamental principle of the web: the URL as a representation of the application's state. When a user navigates through your React applications, libraries like React Router ensure the URL in the address bar updates accordingly. This makes the application state shareable, bookmarkable, and accessible via the browser's back and forward buttons. Without this, the user experience can feel broken and unpredictable.
This lesson focuses on bringing that crucial functionality into our HTMX application. We will cover how to use hx-push-url to update the browser's URL and history during HTMX-driven navigation, fulfilling the second learning outcome in our "Advanced Interactivity and UX" module.
The Problem: AJAX-Powered Amnesia
When you click a link that uses hx-get to swap in new content, HTMX successfully updates the DOM. But from the browser's perspective, no navigation has occurred. The URL in the address bar remains unchanged, and no new entry is added to the browser's history stack. If the user hits the back button, they won't go to the previous view within your app; they'll go to the page they were on before they loaded your application.
Let's watch a short video from Net Ninja that perfectly illustrates this problem and its solution. He's building an "SPA-like" experience for a blog, where clicking an article title loads the article content into the page.
Build an "SPA" with HTMX #3 - Using hx-push-url
This video clearly demonstrates the problem of a static URL during AJAX swaps and introduces hx-push-url as the solution.
First, watch the setup from the beginning to this point. He configures links with hx-get to fetch article content and hx-target="body" to replace the entire page content. This mirrors a full page navigation. Now, watch the critical segment from where he describes the two problems: the URL doesn't update, and the back button is useless. This is the core issue we're solving today. Finally, see the solution in action from this part. By simply adding hx-push-url="true" to the link, HTMX automatically updates the address bar and adds an entry to the browser's history.
How hx-push-url Works
As you just saw, the solution is deceptively simple. But what's happening under the hood? When you use hx-push-url, you're telling HTMX to interact with the browser's History API.
Specifically, HTMX:
- Performs the AJAX request as usual.
- Before swapping the content, it takes a snapshot of the current state of the DOM.
- It pushes the new URL (either the one from the request or one you specify) onto the history stack using
history.pushState(). - It swaps in the new content from the response.
- When the user navigates using the back or forward buttons, HTMX intercepts this and restores the page from its cached DOM snapshot, avoiding another server request.
The official HTMX documentation provides the definitive breakdown of the attribute's options.
</> htmx ~ hx-push-url Attribute
This is the official documentation for the hx-push-url attribute. It's concise and serves as our primary reference.
Read the entire page, paying close attention to the three possible values: true: Pushes the URL of the request (from hx-get, etc.) into the history. This is the most common use case. false: Explicitly prevents a URL push. This is useful for overriding inherited behavior or disabling pushes for certain actions. A URL String: Pushes a specific URL you provide, which can be different from the request URL. Also, review the "Notes" section, as it highlights that the server can also influence this behavior via a response header.
hx-push-url in a CRUD Application
Let's return to the familiar SitePoint contact manager application. It uses hx-push-url extensively to create a seamless navigation experience. Since you've already worked with this codebase, you can focus specifically on how this attribute is applied in different CRUD scenarios.
Build a Full-stack App with Node.js and htmx - SitePoint
This article demonstrates hx-push-url in several practical contexts within an Express.js application.
We'll be looking for hx-push-url='true' and its variations in the Pug templates. Master-Detail View: In the "Displaying a Single Contact" section, examine the sidebar.pug template. Notice how each contact link is given three key attributes. When a user clicks a contact, the content is fetched and targeted to #content, and the URL is updated to /contacts/1 (for example). Form Navigation: The same pattern is used for the "New Contact" link. Find the div.actions in the same section and observe that the link to the 'new' form also uses hx-push-url='true', changing the URL to /contacts/new. Post-Action Navigation (Edit): This is a more advanced pattern. In the "Editing a Contact" section, look at the form.pug template. The form tag itself has a conditional hx-push-url. When you submit an edit (isEditing() is true), the form performs a hx-put. After a successful swap, this attribute pushes the URL of the view page (/contacts/1) to the history, not the URL of the PUT endpoint (/update/1). This is a powerful way to redirect the user to the correct view state after an action. Post-Action Navigation (Delete): Finally, in the "Deleting a Contact" section, look at the delete button's form in the contact.pug template. The button uses hx-delete and also includes hx-push-url='/contacts'. After deleting a contact, this explicitly pushes the user back to the main list URL, cleaning up the history.
These examples from the SitePoint article provide a direct playbook for replacing React Router's functionality in your own CRUD applications.
Opting Out: When Not to Push a URL
Sometimes, you don't want to change the URL. A common case is a form submission that doesn't represent a distinct navigational state. For example, submitting a "create new item" form. The action is a POST to /items, but you likely want the user to remain on the /items page (which is now showing the new item). If the URL were pushed, the user would end up at /items in the address bar but with a history entry for the POST action, which is confusing.
The hx-boost attribute, which we'll cover later, often automatically adds URL pushing. The Net Ninja demonstrates how to override this behavior using hx-push-url="false".
Build an "SPA" with HTMX #5 - Using hx-boost (for Forms)
This video clip shows a practical reason to disable URL pushing on a form.
Watch from this segment. The instructor explains that hx-boost implicitly adds hx-push-url. When he submits a form to create a new article, the URL undesirably changes to the form's action path. He solves this by adding hx-push-url="false" directly to the form element, which overrides the inherited behavior and keeps the URL stable.
This demonstrates an important principle: hx-push-url gives you fine-grained control over the browser's history on an element-by-element basis.
Conclusion
You have now learned how to integrate HTMX's AJAX-driven updates with the browser's native history, creating applications that are both dynamic and well-behaved. By using hx-push-url, you ensure that your application states are linkable, bookmarkable, and that the back button works just as users expect.
Key Takeaways:
hx-push-urlsolves the problem of a static URL and a broken back button in AJAX-heavy applications.- It works by leveraging the browser's History API, pushing a new state and caching the DOM for quick restoration.
- The attribute can take three values:
true(use the request URL), a specific URL string, orfalse(to disable pushing). - You can apply it to links for navigation, and to forms to control the URL state after a submission. This is a direct replacement for the routing logic you are used to from React.
In our previous lesson, we made our UI powerful with OOB swaps. Today, we made it navigable. In the next lesson, we'll make it feel more responsive by addressing what happens during a request. We will explore how to display loading indicators to provide immediate feedback to the user while they wait for the server to respond.