A simpler approach to creating interactive web applications using Python and hypermedia
Modern web development requires complicated dependencies and extensive boilerplate spread over multiple languages to create interactive UIs. FastHTML with HTMX is here to fix that.
Building interactive web applications has always been complicated. I spent years banging my head against the wall with web development. My Python backend code was a thing of beauty - elegant, readable, maintainable. But the moment I needed a simple dropdown to update dynamically? Pure chaos.
The worst part? Most of this complexity was for ridiculously simple interactions.
Traditional approaches force you to choose between:
There had to be a saner approach to building interactive web UIs
FastHTML is a Python library that makes generating HTML intuitive and pythonic. It allows you to:
Div()
, Button()
)Before diving into examples, let's set up a minimal FastHTML application:
from fasthtml.common import *
@rt
def index():
return Div(
H1("Hello, FastHTML!"),
"This is my first FastHTML application.")
serve()
Save this as app.py
and run with python app.py
. Visit http://localhost:5001 to see your application.
đź’ˇ The
@rt
decorator (short for "route") is FastHTML's way of defining HTTP endpoints. It automatically:
FastHTML lets anyone build high-quality, interactive web apps in pure Python. HTMX is a small JavaScript library (< 14KB) that allows you to access modern browser features directly from HTML, rather than writing your own JavaScript. FastHTML is a Python library that makes it easy to generate HTML with HTMX attributes, letting you build dynamic interfaces with minimal code.
Together, they:
For comprehensive documentation see the FastHTML Documentation and HTMX Documentation
Let's see a simple example of a show/hide toggle:
def mk_button(show):
return Button("Hide" if show else "Show",
hx_get="toggle?show=" + ("False" if show else "True"),
hx_target="#content", id="toggle", hx_swap_oob="outerHTML")
@rt
def index(): return Div(mk_button(False), Div(id="content"))
@rt
def toggle(show: bool):
return Div(
Div(mk_button(show)),
Div("Content that is toggled!" if show else ''))
đź’ˇ Tip You can follow the example links to see full running examples and the source code you can use to try it out yourself locally!
With just these few lines of Python, I've created a toggle button that shows and hides content without writing a single line of JavaScript. The hx_get
attribute tells HTMX to make a GET request to the toggle
endpoint when clicked, and the hx_target
attribute tells it to replace the element with ID "content" with the response.
By the end of this post, you'll be able to implement complex UI patterns like inline validation, infinite scrolling, and even real-time chat - all with a fraction of the code you'd need with traditional approaches.
đź’ˇ When to Consider Alternatives
- Highly Interactive UIs: Applications requiring complex client-side state management (like graphic editors)
- Offline-First Applications: Apps that need to function without network connectivity
- Extremely High-Performance Needs: Cases where minimizing network roundtrips is critical (this is not most cases)
Let's start our journey with a simple but powerful pattern: the show/hide toggle. This is a common UI element that allows users to expand and collapse content, saving screen space and reducing visual clutter.
def mk_button(show):
return Button(
"Hide" if show else "Show",
hx_get="toggle?show=" + ("False" if show else "True"),
hx_target="#content", id="toggle",
hx_swap_oob="outerHTML")
@rt
def index():
return Div(mk_button(False), Div(id="content"))
@rt
def toggle(show: bool):
return Div(
Div(mk_button(show)),
Div("Content that is toggled!" if show else ''))
This code creates a simple page with a button that toggles the visibility of a block of text. Let's break down how it works:
The mk_button
function creates a button with HTMX attributes:
hx_get
specifies the endpoint to call when clickedhx_target
identifies which element to update with the responsehx_swap_oob
("out of band") allows us to update multiple elements at onceThe index
route renders the initial state with a "Show" button and an empty content div
The toggle
route handles the button click:
When a user clicks the button, HTMX makes a GET request to our server, which responds with HTML that updates both the button and content area. The entire interaction happens without a page refresh or any custom JavaScript.
What makes this approach powerful is the seamless integration between the client and server. Here's what happens when a user clicks the button:
/toggle?show=True
This pattern - intercepting events, making HTTP requests, and updating the DOM - is the foundation for all HTMX interactions.
But what happens when we need more complex interactions? Let's explore that next.
One of the most common UI patterns in modern web applications is the "click-to-edit" pattern. This allows users to view data in a clean, readable format, then click to transform it into an editable form. When they're done editing, they submit the changes, and the view returns to its original state with updated data. With FastHTML and HTMX, we can implement it with pure Python and a few HTMX attributes.
Here's how we can implement a click-to-edit pattern using FastHTML and HTMX:
flds = dict(firstName='First Name', lastName='Last Name', email='Email')
@dataclass
class Contact:
firstName:str; lastName:str; email:str; edit:bool=False
def __ft__(self):
"The __ft__ method determines how a `Contact` is rendered and displayed"
def item(k, v):
val = getattr(self,v)
return Div(Label(Strong(k), val), Hidden(val, id=v))
return Form(
*(item(v,k) for k,v in flds.items()),
Button('Click To Edit'),
post='form', hx_swap='outerHTML')
contacts = [Contact('Joe', 'Blow', 'joe@blow.com')]
@rt
def index(): return contacts[0]
@rt
def form(c:Contact):
def item(k,v): return Div(Label(k), Input(name=v, value=getattr(c,v)))
return Form(
*(item(v,k) for k,v in flds.items()),
Button('Submit', name='btn', value='submit'),
Button('Cancel', name='btn', value='cancel'),
post="contact", hx_swap='outerHTML'
)
@rt
def contact(c:Contact, btn:str):
if btn=='submit': contacts[0] = c
return contacts[0]
This code creates a contact information display that transforms into an editable form when clicked. Let's break down how it works:
Contact
class with a custom __ft__
method that determines how it renders in FastHTMLform
endpointform
endpoint returns an editable form with the contact's current datacontact
endpointcontact
endpoint updates the data and returns to view modeThe entire interaction happens without a page refresh, giving users a smooth, app-like experience—all with server-rendered HTML.
This example demonstrates several powerful principles:
While our click-to-edit example is already powerful, we can take it a step further with real-time form validation. This is where HTMX truly shines—allowing us to validate form inputs as users type, providing immediate feedback without writing any JavaScript.
Let's see how we can implement real-time validation for a more complex form:
@rt
def index():
return Form(
Div(Label('Email Address', _for='email'),
Input(type='text', name='email', id='email', post='email'),
hx_target='this', hx_trigger='changed', hx_swap='outerHTML'),
Div(Button('Submit', type='submit', id='submit-btn'),
id='submit-btn-container'),
hx_post=submit, hx_target='#submit-btn-container', hx_swap='outerHTML')
@rt
def email(email: str):
error_msg = validate_email(email)
return Div(
Label('Email Address'),
Input(name='email', type='text', value=f'{email}', post='email'),
Div(f'{error_msg}', style='color: red;') if error_msg else None,
hx_target='this',
hx_swap='outerHTML', cls=f"{error_msg if error_msg else 'Valid'}")
@rt
def submit(email: str):
errors = {'email': validate_email(email)}
errors = {k: v for k, v in errors.items() if v is not None}
return Div(
Button("Submit", type='submit', id='submit-btn'),
*[Div(error, style='color: red;') for error in errors.values()],
id='submit-btn-container')
def validate_email(email: str):
email_regex = r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b'
if not re.match(email_regex, email):
return "Please enter a valid email address"
return None
This example creates a form with an email fields, validated in real-time. Let's break down how it works:
post
attribute that triggers a request to a validation endpoint when the value changesThe result is a form that provides immediate feedback to users, guiding them toward valid input without any custom JavaScript. This is a powerful pattern that can be applied to any form in your application.
One of the most counterintuitive aspects of this approach is that server-side validation can feel just as responsive as client-side validation. By making small, targeted requests and updating only the necessary parts of the page, HTMX creates an experience that feels instant to users.
In the next section, we'll explore even more powerful patterns, including loading indicators, infinite scrolling, and real-time updates with WebSockets.
Now that we've explored basic interactions and form validation, let's look at more sophisticated patterns that truly showcase the power of FastHTML and HTMX together.
Infinite scrolling is a pattern that automatically loads more content as the user scrolls down a page. With HTMX, it's remarkably simple compared to other alternatives.
column_names = ('name', 'email', 'id')
def generate_contact(id: int) -> Dict[str, str]:
return {'name': 'Agent Smith',
'email': f'void{str(id)}@matrix.com',
'id': str(uuid.uuid4())}
def generate_table_row(row_num: int) -> Tr:
contact = generate_contact(row_num)
return Tr(*[Td(contact[key]) for key in column_names])
def generate_table_part(part_num: int = 1, size: int = 20) -> Tuple[Tr]:
paginated = [generate_table_row((part_num - 1) * size + i) for i in range(size)]
paginated[-1].attrs.update({
'get': f'page?idx={part_num + 1}',
'hx-trigger': 'revealed',
'hx-swap': 'afterend'})
return tuple(paginated)
@rt
def index():
return Titled('Infinite Scroll',
Div(Table(
Thead(Tr(*[Th(key) for key in column_names])),
Tbody(generate_table_part(1)))))
@rt
def page(idx:int|None = 0):
return generate_table_part(idx)
The magic happens with the hx_trigger="revealed"
attribute, which fires when an element becomes visible in the viewport. When the loading indicator is revealed, HTMX automatically fetches more content and seamlessly adds it to the page.
WebSockets provide a persistent connection between client and server, enabling real-time updates without polling. Setting up WebSockets with FastHTML requires:
ws_connect
attribute to establish connections# All messages here, but only most recent 15 are stored
messages = deque(maxlen=15)
users = {}
# Takes all the messages and renders them
box_style = "border: 1px solid #ccc; border-radius: 10px; padding: 10px; margin: 5px 0;"
def render_messages(messages):
return Div(*[Div(m, style=box_style) for m in messages], id='msg-list')
# Input field is reset via hx_swap_oob after submitting a message
def mk_input(): return Input(id='msg', placeholder="Type your message", value="", hx_swap_oob="true")
@rt
def index():
return Titled("Leave a message for others!"),Div(
Form(mk_input(), ws_send=True), # input field
P("Leave a message for others!"),
Div(render_messages(messages),id='msg-list'), # All the Messages
hx_ext='ws', ws_connect='ws') # Use a web socket
def on_connect(ws, send): users[id(ws)] = send
def on_disconnect(ws):users.pop(id(ws),None)
@app.ws('/ws', conn=on_connect, disconn=on_disconnect)
async def ws(msg:str,send):
await send(mk_input()) # reset the input field immediately
messages.appendleft(msg) # New messages first
for u in users.values(): # Get `send` function for a user
await u(render_messages(messages)) # Send the message to that user
With just these few lines of code, we've created a real-time chat application where messages are instantly broadcast to all connected clients. No JavaScript frameworks, no complex state management, just HTML and Python.
Let's step back and consider what we've accomplished. We've built:
All with minimal code, no JavaScript, and a unified development experience in Python. This approach brings several key benefits:
With FastHTML and HTMX, your entire application lives in Python. This means:
As your application grows, the benefits become even more apparent:
As you start building with FastHTML and HTMX, keep these principles in mind:
Think in HTML, not JavaScript: Focus on the HTML structure and how it changes, not on client-side logic.
Embrace hypermedia: Use links, forms, and HTMX attributes to create interactive applications without custom JavaScript.
Server as the state manager: Let your server handle state transitions and business logic, returning HTML that reflects the new state.
Progressive enhancement: Start with functional HTML and enhance it with HTMX
Now that you understand the basics of FastHTML and HTMX, here are concrete steps to continue your learning:
What will you build with FastHTML and HTMX?
Get notified about new posts on AI, web development, and tech insights.