HTMX is a JavaScript library that can be used to issue AJAX requests, create CSS transisions, and set up web sockets using HTML
The power of HTMX is that it can be used without writing any custom JavaScript code. It works by looking for attributes in HTML tags using that information to set up events, user interaction, and send requests to a back end. The system is backend agnostic and so will essentially work with any system that can accept, interpret, and respond to the requests.
I have been meaning to look at HTMX since hearing about as it seemed like a decent system with a good community. Since then it has been included into Drupal 11.2.0 and there are plans to make it the core system for AJAX requests going forward. As a result it has jumped forward in my list of things to look at.
In this article we will look at how to get HTMX working with a vanilla PHP backend. With a few examples of the two systems working together. I won't be looking at integrating it with Drupal as that will be the subject of a future article.
Installing
HTMX has no dependencies and so just needs to be included in the page. This can be done using a CDN link, which injects the latest verison of the library (2.0.7 as of writing).
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.js" integrity="sha384-ZBXiYtYQ6hJ2Y0ZNoYuI+Nq5MqWBr+chMrS/RkXpNzQCApHEhOt2aY8EJgqwHLkJ" crossorigin="anonymous"></script>
Or, you can download the file from source and include it locally.
<script src="js/htmx.min.js"></script>
The library is only around 50kB (or just 16kB when compressed) so it doesn't add a massive amount of data to the page. Personally, I would go for the download and serve locally option, especially if you are doing some experimentation with the library.
There are also options to include it in your application using npm or Webpack, but as I don't need them for the examples here I won't be exploring those options in this article.
Once the script is loaded onto the page it is ready to use, all we need now is a backend system to accept the requests. To this end I'll create a simple example to demonstrate very simple usage of HTMX.
A Basic Example
A simple example of HTMX in action can be achieved using a button and a div element. What we want to do here is display a message with the current time to the user (inside the div element) when they click on the button. The time will be generated by an AJAX request to the backend PHP webpage.
<button hx-put="/index.php" hx-target="#request-output">Send Request</button>
<div id="request-output"></div>
The buton is where HTMX will generate our event and send the AJAX request, so we add the following attributes to allow this to happen:
- hx-put - This means that HTMX should issue a "PUT" request to the backend path "/index.php" when the button is clicked.
- hx-target - This means that any response from the server should be put inside the element with the ID "request-output", which is the div element we reated.
As you can see from this example, anything that starts with "hx-" is a HTMX attribute. There are are around 10 core attributes and an additional 20 or so more attributes that allow HTMX to interact with our elements indifferent ways. For the full list of attributes available see the HTMX reference page.
When the user clicks the button HTMX will issue a PUT request to the backend. The contents of the index.php file that we are sending the request to are pretty simple. All we need to do is ensure that that the request is coming from HTMX and that the request method is PUT. Once we have verified that the request is correct we can respond.
Any request that comes from HTMX will have a header called HX-Request, which PHP translates into HTTP_HX_REQUEST. This gives us a simple mechanism to test that the request is from HTMX. The PUT method is defined in the REQUEST_METHOD header.
<?php
if (isset($_SERVER['HTTP_HX_REQUEST']) && $_SERVER['HTTP_HX_REQUEST'] === 'true') {
if ($_SERVER['REQUEST_METHOD'] === 'PUT') {
echo '<p>Button clicked at ' . date('r') . '.</p>';
}
}
The response back to the front end just needs to be a printed string, which will be sent upstream to HTMX.
With this in place, when the user clicks the button they will see a message along the lines of "Button clicked at Wed, 22 Oct 2025 21:23:25 +0000.".
Encapsulating PHP Responses
Rather than write lots of PHP code to check for the correct headers we can simplify things a little by encapsulating the checks in a Htmx class. The checks we have written previously can be encapsulated in a class like this.
class Htmx {
public static function isHtmxRequest(): bool
{
return isset($_SERVER['HTTP_HX_REQUEST']) && $_SERVER['HTTP_HX_REQUEST'] === 'true';
}
public static function isPut(): bool
{
return $_SERVER['REQUEST_METHOD'] === 'PUT';
}
}
The PHP code in the previous example can then be rewritten in the following way.
if (Htmx::isHtmxRequest() && Htmx::isPut()) {
echo '<p>Button clicked at ' . date('r') . '.</p>';
}
This works in the same way and looks a bit tidier. Plus, as this only covers a small part of HTMX we can add more methods to the Htmx class as more functionality is required.
Webpage Counter
One experiment I wanted to try out was a simple web page counter. This is an old school approach to analytics where the web page just counts the number of times it has been accessed.
This requires a simple div element with a couple of HTMX attributes.
<div hx-post="index.php" hx-trigger="load delay:500ms">0</div>
The attributes used are:
- hx-post - This means that we will issue a POST request to the index.php endpoint.
- hx-trigger - This tells HTMX to trigger the request if certain conditions are met. In this case we want to trigger the request when the page loads (with the
load trigger) but to also delay the request by 500ms. Essentially, this prevents the request being made immediately and instead waits for half a second. The idea here is to prevent the counter from being triggered by spiders or bots.
The PHP behind the scenes just needs to check that this is a HTMX request and respond appropriately.
$countFile = 'counter.txt';
if (Htmx::isHtmxRequest() && Htmx::isPost()) {
if (!file_exists($countFile)) {
file_put_contents($countFile, 1);
echo 1;
}
else {
$count = (int) file_get_contents($countFile);
$count++;
file_put_contents($countFile, $count);
echo number_format($count);
}
}
The PHP script looks for the existence of the counter.txt and if it exists we load the count from that file. The count is then incremented, saved back into the file, and sent back to the user.
The effect here is pretty simple, but it could be improved upon by adding a transition to the element using the hx-on:* attrbitue so that we can annimate the updated count. We can even use the HX-Current-Url header to make a counter per page, rather than a global counter.
Infinite Scroll
Using HTMX we can quite easily create an infinite scrolling set of results where more results load onto the page as the user scrolls to the bottom.
Here is the initial HTML we will use for this example.
<p>1</p>
<p>2</p>
<p>3</p>
<p>4</p>
<p>5</p>
<p>6</p>
<p>7</p>
<p>8</p>
<p hx-get="index.php?page=1" hx-trigger="revealed" hx-swap="afterend">9</p>
The HTMX attributes in use here are:
- hx-get - Means that we will issue a GET request to the backend.
- hx-trigger - This tells HTMX to trigger the request when a condition has been met. In this case, HTMX will trigger when the element is first rendered into view. This means either loaded onto the page or scrolled into view.
- hx-swap - This attribute controls how the response from the backend will be injected into the page. In this case we are using afterend, which tells HTMX to inject the response after the current element (which in this case is our <p> element).
The server side implementation consists of a loop of 10 items, the last item of which prints out the same <p> element with the same HTMX attributes that we sent to the backend, but this time with an incremented page number. As the last element containing the HTMX attributes is rendered onto the page HTMX will pick up the attributes and create another trigger. When the user scrolls down and uncovers the element containing our trigger it will issue another request and extend the list of items on the page. This creates a list of items that will just go on and on and on, loaded in increments of 10 items at a time by HTMX.
if (Htmx::isHtmxRequest() && Htmx::isGet()) {
$page = $_GET['page'] ?? 0;
$startCount = $page * 10;
$nextPage = $page + 1;
for ($i = $startCount; $i <= $startCount + 9; $i++) {
if ($i == $startCount + 9) {
echo <<<HEREDOC
<p hx-get="/infinitescroll/index.php?page={$nextPage}" hx-trigger="revealed" hx-swap="afterend">{$i}</p>
HEREDOC;
}
else {
echo '<p>' . $i . '</p>';
}
}
}
What's good about this setup is that even if the returned element doesn't extend below the limits of the page it will still trigger the "revealed" trigger and load in an additional group of elements. So, if we just add the first 10 items to the page then HTMX will automatically start loading more elements until it passes below the bottom of the page, where it will stop.
This system is probably better when used with a table element and some actual data, but it shows the effect simply enough, and was quite easy to put together.
Adding A Comment
Using HTMX it is also possible to inject forms into the page and use them for user interaction. As a test I created a simple comment section that uses HTMX to load in the a list of existing comments, and to allow users to post new comments.
The interface created for this consists of a unordered list containing two list items, one of which contains a buton.
<ul>
<li hx-get="/loadcomments.php"
hx-trigger="revealed"
hx-swap="outerHTML">
</li>
<li><button hx-get="/savecomment.php" hx-swap="outerHTML">
Click To Add Comment
</button></li>
</ul>
The list item has the following HTMX attributes.
- hx-get - This will issue a GET request to the file loadcomments.php, which will load the available comments.
- hx-trigger - By using the "revealed" trigger here we load in the comments when the list item is loaded onto the page. This includes if we place the comments section lower down the page and the user scrolls them into view.
- hx-swap - The
hx-swap attribute tells HTMX what to do with the result of the response. In this case outerHTML means that we will fully replace the current item with the result of the response.
The button has the following HTMX attributes.
- hx-get - This will issue a GET request to the file savecomment.php when the user clicks the button.
- hx-swap - Just like the list item above, the value of
outerHTML means that we will replace the button with the result of the response.
When we load the page (or the comments list item scrolls into view) a request is made to loadcomments.php. This PHP script reads the contents of a CSV file (or creates it if the file doesn't exist) and then returns the contents as a list of items to the response, which causes the current list of comments to be displayed.
$commentFile = 'comments.csv';
if (Htmx::isHtmxRequest() && Htmx::isGet()) {
if (!file_exists($commentFile)) {
touch($commentFile);
}
else {
$fh = fopen($commentFile, "r");
while (($data = fgetcsv($fh, 0, ',', '"', '\\')) !== FALSE) {
echo '<li>' . $data[0] . ' - ' . $data[1] . '</li>';
}
fclose($fh);
}
}
When the button is clicked a GET request is made to the savecomment.php script. This responds with a HTML form that contains some HTMX attributes.
if (Htmx::isHtmxRequest() && Htmx::isGet()) {
echo '<form hx-post="/savecomment.php" hx-swap="outerHTML">
<div>
<label>Name</label>
<input type="text" name="name" value="">
</div>
<div>
<label>Comment</label>
<textarea name="comment"></textarea>
</div>
<button type="submit">Submit</button>
</form>';
}
This has the effect of replacing the button with a form that contains a name and comment field, ready for the user to input some information. All of the HTMX attributes used in the form have already been explained, but to explain, a POST request will be made to savecomment.php and the response will replace the HTML of the form.
The POST request sent to the savecomment.php script will contain the information the user entered into the form. We treat the data the user entered into the form for security purposes and then write it into the comments CSV file, once complete we print out the entered comment and the button. This means that when sent back to the user the comment will be visible and the user has the option of adding another comment.
if (Htmx::isHtmxRequest() && Htmx::isPost()) {
$name = $_POST['name'] ?? FALSE;
$comment = $_POST['comment'] ?? FALSE;
if ($name !== FALSE && $comment !== FALSE) {
$name = strip_tags($name);
$comment = substr(strip_tags($comment), 0, 1000);
$fp = fopen($commentFile, 'a');
fputcsv($fp, [$name, $comment], ',', '"', '');
echo '<li>' . $name . ' - ' . $comment . '</li>';
}
echo '<li><button hx-get="/savecomment.php" hx-swap="outerHTML">
Click To Add Comment
</button></li>';
}
This was simple to put together and works very well as a data entry system for comments.
I have added a number of security considerations here, but this is more or less a proof of concept and shouldn't be used for live, production, websites.
Conclusion
As you can see from these examples, I have been able to build some quite powerful features without writing a single line of JavaScript. All of the examples took just a few minutes to put together and most of that effort was spent writing the PHP backend.
What I have put together here just covers the basics of HTMX; the API is quite extensive and there are a number of other features built into the system. If you want to dig deeper I would look at inline form validation, progress bars, and even the "boosting" feature. None of the above examples look at sending back headers to HTMX as well, which is also possible, and changes how HTMX will act on the response from the backend.
In addition to this, it is also possible to add extensions to HTMX to add more functionlity. There are a number of community extensions available that add different features to HTMX. Adding these extensions to HTMX involves just including them into the page.
I've been quite impressed with HTMX both in terms of usability and the community behind the project. The key decision making process of the system has been to create a stable and secure core component, so any most additional functionality is rejected in favor of making extensions.
I will certainly be using HTMX in the future and I am certainly looking forward to using it when it becomes a part of Drupal.
Add new comment