Back to Archive

A Practical Guide to HTMX with Python's Air Framework

September 7, 2025

Building modern, dynamic web apps can feel like choosing between two extremes: a simple, static multi-page app or a complex, JavaScript-heavy SPA. A middle ground exists.

HTMX lets you add dynamic behavior from HTML without much JavaScript. Paired with a Python backend, it forms a productive stack.

This guide covers HTMX's core concepts with interactive examples using Air, a minimalist Python web framework.

What is Air?

Air is a modern Python web framework built on FastAPI. It draws inspiration from Dash's component architecture, Flask's simplicity, FastHTML's HTMX support, and Django's forms and pluggable systems, and more.

It uses a component-based architecture, so you build UIs with reusable Python objects. That keeps code clean and supports autocompletion and type checking.
Because it is built on FastAPI, it can serves both rich HTML components and JSON APIs from the same app. Its Python components make UI code easier to reason about and IDE-friendly thanks to strong typing and docs. It's designed to make web development feel intuitive again.

Now, let's cover HTMX's Big Four and how to use them in air. The full code for this post is here.

Example 1: hx-get

The most fundamental HTMX attribute is hx-get. It tells an element, like a form or a button, to send a GET request to a URL when it's triggered (e.g., when a form is submitted).

By default, HTMX replaces the element that triggered the request with the HTML response from the server.

Let's look at a basic form. When the user enters their name and clicks "Submit," the form will be replaced by a greeting.

The air code:

Loading...

In the basic_form function, we create a <form> component. We add the hx_get="/add-name" attribute to it. This tells HTMX to send a GET request to our /add-name endpoint upon submission. The /add-name endpoint simply returns an <h1> component with the user's name, which then replaces the original form.


Example 2: hx-target

Replacing the entire form isn't always the goal. Often, you want to submit a form and see the results appear in a different part of the page. hx-target solves this.

The hx-target attribute takes a CSS selector and tells HTMX where to place the response instead of replacing the triggering element.

In this example, the form stays visible, and the greeting appears in a <div> below it.

The air code is a small modification of our first example:

Loading...

We added hx_target='#result', pointing to the <div> with id='result'. Now, when the form is submitted, the <h1> from /add-name2 is placed inside our target <div>, leaving the form untouched.


Example 3: hx-swap

By default, hx-target replaces the entire inner content of the target element. But what if you want to add to the content instead of replacing it? That's the job of hx-swap.

The hx-swap attribute controls how the response goes into the DOM. Common values include innerHTML (the default), outerHTML, and, for our use case, values like beforeend (appends the content at the end of the target) and afterbegin (prepends it at the beginning).

Here, each form submission adds a new greeting to the list without removing the old ones.

Here's the code:

Loading...

By setting hx_swap='beforeend', we tell HTMX to append the response from /add-name3 as the last child of our #result div, creating a running list of greetings.


Example 4: hx-trigger

So far, all our requests have been triggered by a form submission. The hx-trigger attribute lets you change what event causes the request. You can trigger requests on clicks, mouseovers, or, in this case, whenever an input's value changes.

This enables "live search" or "live preview" functionality without JavaScript.

In this final example, the result updates in real-time as you type into the input field -- no submit button needed!

This workflow differs. We move the HTMX attributes directly onto the <input> element.

Loading...

We use hx_trigger='input changed'. The input event fires every time the value changes, and changed adds a slight debounce, so it doesn't fire on every single keystroke. The result is a smooth, responsive UI that sends a request to /add-name4 whenever you type, updating the #result div instantly.

Weekly article

Get the weekly deep-dive on context, control, and workflows for useful agents.

5,000+ readers