Dynamic Updates
Fetching new content from the server incrementally
Concepts
- HTMX adds HTTP request behavior to HTML elements through attributes
hx-get="/snails/42"tells HTMX to make a GET request to that URL when the user interacts with the element, without navigating the page- How do people implement this kind of behavior without a library like HTMX??
hx-targetidentifies the element whose content will be replacedhx-swapcontrols how the replacement happens (replace inner HTML, replace the outer element, append after, etc.)- If you omit
hx-target, which element's content does HTMX replace by default?
- A partial response returns only an HTML fragment
(a detail panel, a table row) rather than a complete page
- The server detects HTMX requests via the
HX-Requestheader and returns the appropriate fragment - What happens if a user copies the URL of an HTMX partial response and opens it directly in a new browser tab?
- The server detects HTMX requests via the
- HTMX handles server-fetched content updates;
Alpine.js handles local client-side state (show/hide, toggle, character count)
- They solve different problems and work well together on the same page
- Could you implement the snail detail panel using Alpine.js and a
fetch()call instead of HTMX? What would you gain and what would you lose?
- A full page reload navigates away and re-renders everything;
a partial update replaces only a section of the page
- Partial updates are faster, preserve scroll position, and avoid the flash of a blank page
- If a user is halfway down a long page and an HTMX request updates only the top of the page, where does the scroll position end up?
What HTMX does
hx-get,hx-post,hx-target, andhx-swapattributes- Add
hx-get,hx-target, andhx-swapattributes to a table row so that clicking it fetches the detail panel for that snail
- Add
- How HTMX extends HTML rather than replacing it with a separate framework
- Contrast implementing the same fetch-and-update behavior using plain
fetch()in JavaScript versus using HTMX attributes
- Contrast implementing the same fetch-and-update behavior using plain
Including HTMX
- Use a script tag (the same CDN pattern as Alpine.js)
- Add the HTMX CDN script tag to the page template alongside the Alpine.js tag
Loading a detail panel on demand
- Clicking a row triggers an HTMX request
- Add HTMX attributes to each table row so that clicking it sends a GET request for that snail's detail panel
- The server returns an HTML fragment
- Add a Litestar route that detects the
HX-Requestheader and returns only the detail panel fragment rather than a full page
- Add a Litestar route that detects the
- HTMX swaps it into the page without a full reload
- Update the page template to include a placeholder element where HTMX will insert the detail panel
HTMX and Alpine.js together
- The two tools solve different problems
- HTMX: fetching content from the server
- Alpine.js: managing local browser state
- Which parts of the snail application page are best handled by HTMX and which by Alpine.js
- A diagram of which part of the page each controls and where the boundary lies
- Draw a diagram showing which UI regions are controlled by HTMX, which by Alpine.js, and where the two interact
Check for Understanding
What is the difference between a full page reload and an HTMX partial update from the user's perspective? From the server's perspective?
From the user's perspective:
a full reload navigates away from the current page and reloads everything,
causing a visible flash and resetting scroll position.
A partial update replaces only a section of the page,
which is faster and less disruptive.
From the server's perspective:
a full reload receives a regular browser request and must return a complete HTML page.
An HTMX request includes an HX-Request: true header;
the server can detect this and return only the relevant fragment.
What does hx-target="#detail-panel" tell HTMX to do?
It tells HTMX to put the response content into the element with id="detail-panel",
using whatever swap strategy hx-swap specifies (the default is to replace the inner HTML).
Without hx-target,
HTMX replaces the content of the element that triggered the request.
How does the server know a request came from HTMX rather than a regular browser navigation?
HTMX adds an HX-Request: true HTTP header to every request it makes.
The server can inspect this header to decide whether to return a full page or just a fragment.
In Litestar,
you can read request headers in a handler;
some frameworks also provide middleware that detects this header automatically.
If Alpine.js manages local state and HTMX fetches server content, which tool would you use for each of these: (a) a dropdown that shows/hides options, (b) loading updated search results from the server?
(a) Alpine.js: showing and hiding a dropdown does not require a server round-trip;
it is purely local state (open = true/false) that controls visibility.
(b) HTMX: fetching updated search results requires sending the query to the server and getting back fresh data;
HTMX triggers the request and swaps the result into the page.
Exercises
Inline delete
Add an HTMX-powered delete button to each table row. When clicked, it should send a DELETE request to the server. The server should delete the record and return an empty response; HTMX should remove the row from the page without a full reload. Test that a refresh does not re-add the deleted row.
Search-as-you-type
Add hx-trigger="keyup delay:300ms" to the search input
so HTMX sends a new request 300 milliseconds after the user stops typing.
The server should return only the table body fragment.
Explain why the 300ms delay is there.
Loading indicators
Add a spinner (a CSS animation or a simple text message like "Loading...")
that appears while an HTMX request is in flight and disappears when the response arrives.
Use the hx-indicator attribute to connect the spinner to the request.
Test it by adding an artificial delay to the server.
Compare approaches
Implement loading a detail panel for a row in two ways: (1) as a regular link that navigates to a new page, and (2) using HTMX. Which is faster? Which preserves scroll position? Which is more code? Which would you choose for this application and why?