
FastHTML is an innovative Python-based web framework designed to make web development accessible and enjoyable for beginners while providing robust tools for seasoned developers. By blending Python’s simplicity with HTML-like syntax, FastHTML allows you to create dynamic and responsive web applications without wrestling with complex setups or unfamiliar languages.
Why FastHTML?
Unlike traditional web development that requires knowledge of HTML, CSS, JavaScript and possibly frameworks like React or Vue, FastHTML lets you build complete web applications using just Python. Here’s why it’s particularly valuable for beginners:
- Single Language: Build both backend and frontend with just Python
- Simpler Than Alternatives: More approachable than Django or Flask for UI development
- Hypermedia-Driven: Built-in support for HTMX allows interactivity without JavaScript
- Python-Native Syntax: Use familiar Python functions instead of learning template languages
- No Build Tools: No need for npm, webpack, or other JavaScript build tools
This article serves as your entry point into FastHTML, guiding you through installation, basic syntax, and the use of various components with practical examples. By the end, you’ll be equipped to build your first FastHTML project with confidence.
FastHTML Series
Below are the articles on FastHTML to help you get started:
Installing FastHTML
Before diving into coding, you need to set up FastHTML on your machine. The first prerequisite is Python, version 3.7 or higher. If you don’t have Python installed, head to the Python install on MAC and download the latest version compatible with your operating macOS. Follow the installation prompts, ensuring you check the option to add Python to your system’s PATH, which makes running Python commands easier from the terminal.
If you have other OS than Mac you should check the python website for the tutorial.
With Python ready, installing FastHTML is a breeze. Open your terminal (Command Prompt on Windows, Terminal on macOS/Linux) and follow these steps:
1. Create a virtual environment for the project and activate it
python3 -m venv fhenv
source fhenv/bin/activate
# On Windows:
# fhenv\Scripts\activate
This creates an isolated environment for your project, preventing package conflicts. A virtual environment is like a separate, clean installation of Python where you can install packages without affecting your system Python installation.
2. Install FastHTML
pip install python-fasthtml
This fetches the FastHTML package and its dependencies. You can also check the official FastHTML documentation for more details.
Understanding FastHTML Syntax
FastHTML’s standout feature is its ability to let you write web pages using Python functions that mimic HTML tags. Instead of juggling separate HTML files, you define your page structure directly in Python:
from fasthtml.common import *
# Creating a paragraph element
paragraph = P("Hello, World!")
In this snippet, P
is a FastHTML function that generates an HTML <p>
tag with “Hello, World!” as its content. This gets converted to <p>Hello, World!</p>
when rendered. FastHTML provides similar functions for all standard HTML elements—H1
for headings, Div
for divisions, Ul
and Li
for lists, and so on.
Here’s a quick mapping of some common HTML elements to FastHTML functions:
HTML | FastHTML | Example |
---|---|---|
<p> | P() | P("Text") |
<h1> | H1() | H1("Heading") |
<div> | Div() | Div(P("Child element")) |
<a> | A() | A("Link text", href="https://example.com") |
<input> | Input() | Input(type="text", name="username") |
To build a complete webpage, you combine these functions into a structure:
page = Html(
Head(
Title("My First Page")
),
Body(
H1("Welcome to FastHTML"),
P("This is a simple page built with FastHTML.")
)
)
This code produces a complete HTML document with a title, heading, and paragraph. The nested structure mirrors HTML’s hierarchy, making it easy to visualize how components fit together.
How FastHTML Functions Work
Each FastHTML function takes:
- Positional arguments for child elements or content
- Keyword arguments for HTML attributes
For example, in P("Hello", cls="greeting")
:
"Hello"
is the content of the paragraphcls="greeting"
becomes the HTML attributeclass="greeting"
Note: We use cls
instead of class
because class
is a reserved keyword in Python.
Building Your First Web Page
Let’s put this into action by creating a simple web page. Create a file called main.py
and add the following code:
from fasthtml.common import *
# Create a FastHTML application
app = FastHTML()
# Define a route for the root URL "/"
@app.get("/")
def home():
page = Html(
Head(
Title("Getting Started with FastHTML"),
Script(src="https://cdn.tailwindcss.com") # Including Tailwind CSS for styling
),
Body(
H1("FastHTML Basics", cls="text-2xl font-bold mb-4"),
P("Below is a list of features:", cls="mb-2"),
Ul(
Li("Easy to learn"),
Li("Python-based"),
Li("Dynamic and responsive")
)
)
)
return page
# Start the FastHTML server
if __name__ == "__main__":
serve()
Let’s break this down:
- We import all FastHTML components with
from fasthtml.common import *
- We create a FastHTML application with
app = FastHTML()
- We define a route handler for the root URL (
/
) using the@app.get("/")
decorator - Our
home()
function returns a complete HTML page structure - The
serve()
function starts the FastHTML server
Save the file, then run it from your terminal:
python main.py
You should see output similar to:
Link: http://localhost:5001
INFO: Will watch for changes in these directories: ['/path/to/your/project']
INFO: Uvicorn running on http://0.0.0.0:5001 (Press CTRL+C to quit)
Open your browser and navigate to http://localhost:5001
. You’ll see a page with a heading, paragraph, and bullet list. The @app.get("/")
decorator tells FastHTML to serve this page at the root URL.
What’s Happening Behind the Scenes
When you visit http://localhost:5001
:
- FastHTML receives the request for the root URL (
/
) - It calls the
home()
function - The function returns a page structure built with FastHTML components
- FastHTML converts these components to HTML
- The HTML is sent to your browser
FastHTML handles all the HTTP server details so you can focus on building your UI.
Basic UI Components
Let’s explore the fundamental FastHTML components you can use to build interfaces. We’ll start with the basics and progressively build more complex UIs.
Text Elements
Text elements are the foundation of any interface. FastHTML makes creating them intuitive:
# Headings
heading1 = H1("Main Heading", cls="text-2xl font-bold")
heading2 = H2("Subheading", cls="text-xl font-semibold")
# Paragraphs
paragraph = P("This is a paragraph of text.", cls="mb-4")
# Formatted text
bold_text = Strong("This text is bold")
italic_text = Em("This text is italicized")
Here’s a complete example showing various text elements:
@app.get("/text-elements")
def text_elements():
return Html(
Head(Title("Text Elements")),
Body(
H1("Heading Level 1", cls="text-3xl font-bold"),
H2("Heading Level 2", cls="text-2xl font-semibold"),
H3("Heading Level 3", cls="text-xl font-medium"),
P("This is a regular paragraph with some ",
Strong("bold text"), " and some ",
Em("italicized text"), " mixed in."),
P("You can also use ", Code("code snippets"), " inline.")
)
)
When rendered, this creates a hierarchy of text elements with different sizes and styles. The cls
attribute sets CSS classes that style the elements. In this example, we’re using Tailwind CSS classes like text-3xl
(for font size) and font-bold
(for font weight).
Containers and Layout
Organizing content is key to good UI design. FastHTML provides container elements for structuring your page:
# Basic container
container = Div(
H2("Section Title"),
P("Content inside a container"),
cls="p-4 bg-gray-100 rounded"
)
# Grid layout (with Tailwind CSS)
grid = Div(
Div(P("Column 1"), cls="p-2"),
Div(P("Column 2"), cls="p-2"),
Div(P("Column 3"), cls="p-2"),
cls="grid grid-cols-3 gap-4"
)
The Div
component is extremely versatile for creating containers and layout structures. When combined with CSS frameworks like Tailwind, you can create responsive layouts with minimal effort.
Here’s a layout example using nested containers:
@app.get("/layout")
def layout_demo():
return Html(
Head(
Title("Layout Demo"),
Script(src="https://cdn.tailwindcss.com")
),
Body(
Div(
H1("Page Layout Example", cls="text-2xl font-bold mb-4"),
# Main layout grid with sidebar and content
Div(
# Sidebar
Div(
H2("Sidebar", cls="text-xl mb-2"),
Ul(
Li("Home"),
Li("About"),
Li("Services"),
Li("Contact"),
cls="space-y-2"
),
cls="bg-gray-100 p-4 rounded"
),
# Main content
Div(
H2("Main Content", cls="text-xl mb-2"),
P("This is the main content area of our layout example."),
P("You can structure complex layouts using nested Div elements."),
cls="bg-white p-4 rounded"
),
# Grid with 1 column on mobile, 4 columns on medium screens and up
cls="grid grid-cols-1 md:grid-cols-4 gap-4"
),
cls="container mx-auto p-4"
)
)
)
This creates a responsive layout with:
- A sidebar that contains navigation links
- A main content area
- A layout that adapts to different screen sizes (1 column on mobile, 4 columns on larger screens)
Links and Buttons
Interactive elements like links and buttons allow users to navigate and take actions:
# Basic link
link = A("Visit Google", href="https://google.com", cls="text-blue-500 hover:underline")
# Button
button = Button("Click Me", cls="bg-blue-500 text-white px-4 py-2 rounded")
The A
component creates HTML anchor tags (<a>
) for links, while the Button
component creates HTML button elements (<button>
).
Let’s create a navigation bar with links and buttons:
@app.get("/navigation")
def navigation_demo():
return Html(
Head(
Title("Navigation Demo"),
Script(src="https://cdn.tailwindcss.com")
),
Body(
# Navigation bar
Div(
Div(
# Logo/site name
A("FastHTML Demo", href="/", cls="text-xl font-bold text-white"),
# Navigation links and login button
Div(
A("Home", href="/", cls="text-white hover:text-gray-200 mx-2"),
A("Features", href="/features", cls="text-white hover:text-gray-200 mx-2"),
A("Docs", href="/docs", cls="text-white hover:text-gray-200 mx-2"),
Button("Login", cls="bg-white text-blue-600 px-3 py-1 rounded ml-4"),
cls="flex items-center"
),
cls="flex justify-between items-center"
),
cls="bg-blue-600 p-4"
),
# Page content
Div(
H1("Welcome to FastHTML", cls="text-3xl font-bold mb-4"),
P("This example shows a navigation bar with links and a button."),
cls="container mx-auto p-4"
)
)
)
This creates a navigation bar with:
- A site logo/name on the left
- Navigation links in the center
- A login button on the right
- Hover effects on the links
- A clean, modern appearance thanks to Tailwind CSS
Forms and Inputs
Forms allow users to input data. FastHTML makes creating forms straightforward:
# Text input
text_input = Input(type="text", name="username", placeholder="Enter username")
# Password input
password_input = Input(type="password", name="password", placeholder="Enter password")
# Complete form
login_form = Form(
Label("Username:", Input(type="text", name="username")),
Label("Password:", Input(type="password", name="password")),
Button("Submit", type="submit"),
action="/submit",
method="post"
)
The Form
component creates HTML form elements, while Input
creates various input types based on the type
attribute. The action
attribute specifies where the form data will be sent, and the method
attribute specifies the HTTP method (GET or POST).
Let’s create a complete contact form:
@app.get("/contact")
def contact_form():
return Html(
Head(
Title("Contact Form"),
Script(src="https://cdn.tailwindcss.com")
),
Body(
Div(
H1("Contact Us", cls="text-2xl font-bold mb-4"),
# Contact form
Form(
# Name field
Div(
Label("Name:", For="name", cls="block mb-1"),
Input(type="text", id="name", name="name", placeholder="Your name",
cls="w-full p-2 border rounded mb-3"),
cls="mb-4"
),
# Email field
Div(
Label("Email:", For="email", cls="block mb-1"),
Input(type="email", id="email", name="email", placeholder="Your email",
cls="w-full p-2 border rounded mb-3"),
cls="mb-4"
),
# Message field
Div(
Label("Message:", For="message", cls="block mb-1"),
Textarea(id="message", name="message", placeholder="Your message", rows=5,
cls="w-full p-2 border rounded mb-3"),
cls="mb-4"
),
# Submit button
Button("Send Message", type="submit",
cls="bg-blue-500 text-white px-4 py-2 rounded hover:bg-blue-600"),
# Form attributes
action="/submit-contact",
method="post",
cls="max-w-md mx-auto bg-gray-50 p-6 rounded shadow"
),
cls="container mx-auto p-4"
)
)
)
This creates a styled contact form with:
- Text input for name
- Email input for email address
- Textarea for the message
- Submit button
- Form submission handling to “/submit-contact”
- Proper styling and layout for all elements
Lists and Tables
Organizing data with lists and tables is common in web applications:
# Unordered list
unordered_list = Ul(
Li("Item 1"),
Li("Item 2"),
Li("Item 3")
)
# Ordered list
ordered_list = Ol(
Li("First item"),
Li("Second item"),
Li("Third item")
)
# Basic table
table = Table(
Thead(
Tr(
Th("Name"),
Th("Email"),
Th("Role")
)
),
Tbody(
Tr(
Td("John Doe"),
Td("[email protected]"),
Td("Admin")
),
Tr(
Td("Jane Smith"),
Td("[email protected]"),
Td("User")
)
)
)
The Ul
and Ol
components create unordered and ordered lists, while Li
creates list items. The Table
, Thead
, Tbody
, Tr
, Th
, and Td
components create HTML table elements.
Here’s a data table example:
@app.get("/data-table")
def data_table():
return Html(
Head(
Title("Data Table"),
Script(src="https://cdn.tailwindcss.com")
),
Body(
Div(
H1("User Data", cls="text-2xl font-bold mb-4"),
# User data table
Table(
# Table header
Thead(
Tr(
Th("ID", cls="p-2 border"),
Th("Name", cls="p-2 border"),
Th("Email", cls="p-2 border"),
Th("Role", cls="p-2 border"),
Th("Actions", cls="p-2 border"),
cls="bg-gray-100"
)
),
# Table body
Tbody(
# Row 1
Tr(
Td("1", cls="p-2 border"),
Td("John Doe", cls="p-2 border"),
Td("[email protected]", cls="p-2 border"),
Td("Admin", cls="p-2 border"),
Td(Button("Edit", cls="bg-blue-500 text-white px-2 py-1 rounded mr-2"),
Button("Delete", cls="bg-red-500 text-white px-2 py-1 rounded"),
cls="p-2 border"),
),
# Row 2
Tr(
Td("2", cls="p-2 border"),
Td("Jane Smith", cls="p-2 border"),
Td("[email protected]", cls="p-2 border"),
Td("User", cls="p-2 border"),
Td(Button("Edit", cls="bg-blue-500 text-white px-2 py-1 rounded mr-2"),
Button("Delete", cls="bg-red-500 text-white px-2 py-1 rounded"),
cls="p-2 border"),
),
# Row 3
Tr(
Td("3", cls="p-2 border"),
Td("Robert Johnson", cls="p-2 border"),
Td("[email protected]", cls="p-2 border"),
Td("Editor", cls="p-2 border"),
Td(Button("Edit", cls="bg-blue-500 text-white px-2 py-1 rounded mr-2"),
Button("Delete", cls="bg-red-500 text-white px-2 py-1 rounded"),
cls="p-2 border"),
)
),
cls="w-full border-collapse"
),
cls="container mx-auto p-4 overflow-x-auto"
)
)
)
This creates a styled data table with:
- Column headers (ID, Name, Email, Role, Actions)
- Multiple rows of data
- Action buttons in the last column
- Proper styling for all elements
- Horizontal scrolling for small screens
Adding Interactivity with HTMX
FastHTML seamlessly integrates with HTMX, a library that allows you to access AJAX, CSS Transitions, WebSockets and Server Sent Events directly in HTML, without writing JavaScript. FastHTML includes HTMX by default, so there’s no need to import it separately in most cases.
Here’s a simple counter example:
from fasthtml.common import *
app = FastHTML()
# A simple counter variable to demonstrate state
counter = 0
@app.get("/")
def home():
return Titled("HTMX Counter Example",
Div(
H1("HTMX Counter", cls="text-2xl font-bold mb-4"),
Div(
# Counter display with unique ID for targeting
P(f"Current count: {counter}", id="counter", cls="text-xl mb-4"),
# Increment button with HTMX attributes
Button("Increment",
hx_post="/increment",
hx_target="#counter",
cls="bg-blue-500 text-white px-4 py-2 rounded mr-2"),
# Decrement button with HTMX attributes
Button("Decrement",
hx_post="/decrement",
hx_target="#counter",
cls="bg-red-500 text-white px-4 py-2 rounded"),
cls="p-4 bg-gray-100 rounded"
),
cls="container mx-auto p-4"
),
Script(src="https://cdn.tailwindcss.com")
)
# Handler for increment button
@app.post("/increment")
def increment():
global counter
counter += 1
# Return just the counter element, not the whole page
return P(f"Current count: {counter}", id="counter", cls="text-xl mb-4")
# Handler for decrement button
@app.post("/decrement")
def decrement():
global counter
counter -= 1
# Return just the counter element, not the whole page
return P(f"Current count: {counter}", id="counter", cls="text-xl mb-4")
serve()
Understanding HTMX Attributes
FastHTML provides special attributes for HTMX integration:
hx_post
- Sends a POST request to the specified URL when the element is clickedhx_get
- Sends a GET request to the specified URL when the element is clickedhx_target
- Specifies which element to update with the response (using CSS selector syntax)hx_swap
- Controls how the response is swapped in (e.g., “innerHTML”, “outerHTML”, “beforeend”)hx_trigger
- Specifies when to trigger the request (e.g., “click”, “change”, etc.)
In FastHTML, these attributes are provided as Python parameters, with underscores replacing hyphens (e.g., hx_post
instead of hx-post
).
How the Counter Works
- When you click the “Increment” button, HTMX sends a POST request to
/increment
- The server runs the
increment()
function, which increases the counter value - The function returns just the updated paragraph element
- HTMX replaces the content of the element with id=“counter” with the response
- The page updates without a full refresh
This pattern is powerful for creating interactive web applications without writing JavaScript.
Building a Todo Application
Let’s combine everything we’ve learned to build a simple todo application:
from fasthtml.common import *
from dataclasses import dataclass
app = FastHTML()
# Our simple data store
todos = []
todo_id_counter = 0
# Define the data structure for a todo item
@dataclass
class Todo:
id: int
title: str
completed: bool = False
@app.get("/")
def home():
return Titled("FastHTML Todo App",
Div(
H1("Todo Application", cls="text-2xl font-bold mb-4"),
# Add new todo form
Form(
Div(
Input(type="text", name="title", placeholder="Add a new todo",
cls="p-2 border rounded w-full md:w-80"),
Button("Add", type="submit",
cls="bg-blue-500 text-white px-4 py-2 rounded ml-2"),
cls="flex items-center mb-4"
),
# When the form is submitted, send a POST request to /add-todo
hx_post="/add-todo",
# Update the element with id="todo-list"
hx_target="#todo-list",
# Add the new todo at the end of the list
hx_swap="beforeend"
),
# Todo list container
Div(
id="todo-list",
cls="space-y-2"
),
cls="container mx-auto p-4 max-w-md"
),
Script(src="https://cdn.tailwindcss.com")
)
# Handler for adding a new todo
@app.post("/add-todo")
def add_todo(title: str):
global todo_id_counter
# Skip if the title is empty
if not title.strip():
return ""
# Create a new todo and add it to the list
todo_id_counter += 1
new_todo = Todo(id=todo_id_counter, title=title)
todos.append(new_todo)
# Return the HTML for the new todo item
return create_todo_item(new_todo)
# Handler for toggling a todo's completed status
@app.post("/toggle-todo/{id}")
def toggle_todo(id: int):
for todo in todos:
if todo.id == id:
# Toggle the completed status
todo.completed = not todo.completed
# Return the updated todo item HTML
return create_todo_item(todo)
return ""
# Handler for deleting a todo
@app.delete("/delete-todo/{id}")
def delete_todo(id: int):
global todos
# Remove the todo with the specified id
todos = [todo for todo in todos if todo.id != id]
# Return an empty string since we're removing the element
return ""
# Helper function to create the HTML for a todo item
def create_todo_item(todo: Todo):
# Add strikethrough style if the todo is completed
completed_class = "line-through text-gray-500" if todo.completed else ""
return Div(
Div(
# Checkbox for marking the todo as completed
Input(type="checkbox",
checked=todo.completed,
hx_post=f"/toggle-todo/{todo.id}",
hx_target=f"#todo-{todo.id}",
hx_swap="outerHTML",
cls="mr-2"),
# Todo title
Span(todo.title, cls=completed_class),
cls="flex-grow"
),
# Delete button
Button("×",
hx_delete=f"/delete-todo/{todo.id}",
hx_target=f"#todo-{todo.id}",
hx_swap="outerHTML",
cls="text-red-500 font-bold"),
# Unique ID for targeting this todo item
id=f"todo-{todo.id}",
cls="flex items-center p-2 border rounded"
)
serve()
How the Todo App Works
- Data Structure: We use a Python dataclass to define the structure of a todo item
- UI Structure: The main page has a form for adding todos and a container for displaying them
- Adding Todos:
- The form sends a POST request to
/add-todo
when submitted - The server creates a new todo and returns the HTML for it
- HTMX adds the new todo to the end of the list
- The form sends a POST request to
- Toggling Todos:
- The checkbox sends a POST request to
/toggle-todo/{id}
when clicked - The server toggles the todo’s completed status and returns the updated HTML
- HTMX replaces the todo item with the updated version
- The checkbox sends a POST request to
- Deleting Todos:
- The delete button sends a DELETE request to
/delete-todo/{id}
when clicked - The server removes the todo from the list
- HTMX removes the todo item from the page
- The delete button sends a DELETE request to
This demonstrates how FastHTML can be used to build a complete interactive application with minimal code.
The FastHTML Advantage
Now that you’ve seen FastHTML in action, let’s summarize its key advantages:
- Python-Powered UI: Write both your backend and frontend in Python
- Declarative Syntax: Create UIs by composing functions rather than writing HTML templates
- Integrated Interactivity: Built-in support for HTMX makes adding interactivity simple
- No Context Switching: Stay in Python throughout your development workflow
- Minimal Dependencies: No need for a complex JavaScript stack
- Quick Development: Build functional UIs in minutes rather than hours
FastHTML is particularly well-suited for:
- Internal tools and dashboards
- Prototypes and MVPs
- Data visualization applications
- Admin interfaces
- Any application where development speed is prioritized over complex UI interactions
Conclusion
FastHTML empowers beginners to craft web applications using Python’s familiar syntax, sidestepping the complexities of traditional web development. In this guide, we’ve walked through installing FastHTML, mastering its HTML-like syntax, and building various UI components from simple text elements to complete interactive applications.
You’ve seen how to:
- Create basic HTML elements using Python functions
- Structure layouts with containers and grids
- Build forms for user input
- Create interactive UIs with HTMX integration
With FastHTML, you can focus on your application’s functionality rather than wrestling with multiple languages and frameworks. Its intuitive approach makes web development more accessible while providing the power and flexibility needed for real-world applications.