Type-safe, composable UI components for Python — server-side rendered, HTMX-first, 20+ pre-built components.
Project description
htmforge
Type-safe, composable HTML components for Python — server-side rendered, HTMX-first, framework-agnostic.
Build HTML entirely in Python. No templates, no string formatting, no XSS surprises. Validated props via Pydantic, typed HTMX attributes, and direct adapters for FastAPI, Flask, and Django.
Why htmforge?
# ❌ Before — string templates, no type safety, easy to get wrong
html = f'<div class="alert {variant}"><p>{message}</p></div>'
# ✅ After — validated props, typed attributes, XSS-safe by default
Alert(variant=AlertVariant.SUCCESS, content=message).to_html()
- Type-safe props via Pydantic v2 — validated on construction and assignment
- XSS protection built-in —
markupsafeescapes all text content automatically - HTMX-native — typed enums for every
hx-*attribute, no string guessing - 20+ pre-built components — Alerts, DataTables, Forms, Modals, Spinners, Tabs, and more
- Framework adapters —
to_fastapi(),to_flask(),to_django()out of the box - 2 dependencies — only
pydanticandmarkupsafe; FastAPI/Flask/Django are optional - py.typed — full inline type stubs, works perfectly with mypy strict and pyright
Installation
pip install htmforge
Requires Python 3.11+. No extra dependencies for core usage.
60-second example
from htmforge import Component
from htmforge.elements import div, h1, p
class UserCard(Component):
name: str
email: str
def render(self):
return div(
h1(self.name),
p(self.email, cls="text-muted"),
cls="card",
)
print(UserCard(name="Ada Lovelace", email="ada@example.com").to_html())
# <div class="card"><h1>Ada Lovelace</h1><p class="text-muted">ada@example.com</p></div>
Pre-built Components
htmforge ships with 20+ production-ready components, all with typed props and HTMX support.
Layout & Structure
| Component | Description | Import |
|---|---|---|
Page |
Abstract full-page component — emits <!DOCTYPE html> |
from htmforge.components.page import Page |
Data Display
| Component | Description | Import |
|---|---|---|
Alert |
Info / success / warning / error box, dismissible | from htmforge.components import Alert |
Badge |
Small inline label with variant colors | from htmforge.components import Badge |
Breadcrumb |
Ordered nav with aria-current for active item |
from htmforge.components import Breadcrumb |
DataTable |
List/dict rows, sortable headers, HTMX reload | from htmforge.components import DataTable, ColumnDef |
Pagination |
Previous/Next + numbered page links, HTMX target | from htmforge.components import Pagination |
Toast |
Timed notifications with OOB swap support | from htmforge.components import Toast |
Navigation & Interaction
| Component | Description | Import |
|---|---|---|
Accordion |
Collapsible sections using <details>/<summary> |
from htmforge.components import Accordion |
Dropdown |
Trigger button with HTMX-toggled menu | from htmforge.components import Dropdown |
Modal |
Trigger button + <dialog> overlay, HTMX-loaded body |
from htmforge.components import Modal |
SearchInput |
Text input with keyup debounce via HTMX |
from htmforge.components import SearchInput |
Spinner |
Accessible loading indicator (SM / MD / LG) | from htmforge.components import Spinner, SpinnerSize |
Tabs |
Tab strip with HTMX lazy-load per inactive tab | from htmforge.components import Tabs |
Forms & Input
| Component | Description | Import |
|---|---|---|
Form |
Full form with auto-error injection and HTMX submit | from htmforge.components import Form |
FormField |
Label + input + optional error block, 8 input types | from htmforge.components import FormField, InputType |
CheckboxField |
Single checkbox with label and error display | from htmforge.components import CheckboxField |
SelectField |
Dropdown <select> with typed options |
from htmforge.components import SelectField |
RadioGroup |
Radio button group with <fieldset> and legend |
from htmforge.components import RadioGroup |
FormGroup |
Layout container for multiple form fields | from htmforge.components import FormGroup |
HTMX Integration
Every hx-* attribute is a typed enum — no misspelled strings, full IDE autocompletion.
from htmforge.elements import button, input
from htmforge.htmx import HxSwap, HxTarget, hx_keyup_delay
# Delete button with confirmation
btn = button(
"Delete",
hx_delete="/items/1",
hx_swap=HxSwap.OUTER_HTML,
hx_target=HxTarget.CLOSEST_TR,
hx_confirm="Really delete this item?",
)
# → <button hx-delete="/items/1" hx-swap="outerHTML"
# hx-target="closest tr" hx-confirm="Really delete this item?">Delete</button>
# Debounced search input
search = input(
type="search",
name="q",
hx_get="/search",
hx_trigger=hx_keyup_delay(300), # → "keyup delay:300ms"
hx_target="#results",
placeholder="Search...",
)
Available enums: HxSwap, HxTrigger, HxTarget, HxPushUrl
Framework Adapters
FastAPI
from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from htmforge.components.page import Page
from htmforge.elements import div, h1
app = FastAPI()
class HomePage(Page):
def _body_content(self):
return [div(h1("Hello from htmforge"))]
@app.get("/", response_class=HTMLResponse)
def index():
return HomePage(title="Home").to_html()
Flask
from flask import Flask
from htmforge.components.page import Page
from htmforge.elements import div, h1
app = Flask(__name__)
class HomePage(Page):
def _body_content(self):
return [div(h1("Hello from htmforge"))]
@app.route("/")
def index():
return HomePage(title="Home").to_flask() # Returns Flask Response directly
Django
from htmforge.components.page import Page
from htmforge.elements import div, h1
class HomePage(Page):
def _body_content(self):
return [div(h1("Hello from htmforge"))]
def index(request):
return HomePage(title="Home").to_django() # Returns HttpResponse directly
Form with Validation Errors
The Form component automatically routes validation errors to the matching field by name:
from htmforge.components import Form, FormField, InputType
form = Form(
action="/register",
fields=[
FormField(name="username", label_text="Username", input_type=InputType.TEXT),
FormField(name="email", label_text="Email", input_type=InputType.EMAIL),
],
errors={
"email": "This email is already registered.",
},
submit_label="Create Account",
)
# The email field automatically renders its error block — no manual wiring needed.
Elements
htmforge.elements provides factory functions for all 80+ HTML5 elements. Python attribute names are mapped automatically:
| Python | HTML output |
|---|---|
cls="btn" |
class="btn" |
hx_get="/url" |
hx-get="/url" |
data_id="1" |
data-id="1" |
required=True |
required (boolean flag) |
disabled=False |
(omitted) |
from htmforge.elements import form, input, button, label
el = form(
label("Search", for_="q"),
input(id="q", type="search", name="q", hx_get="/search", hx_target="#results"),
button("Go", type="submit"),
cls="search-form",
hx_boost="true",
)
All text content is escaped by markupsafe — safe by default, opt-out with safe_html() or raw() for trusted content.
API Helpers
from htmforge import render, when
from htmforge.elements import div, p
# render() — top-level convenience, works on Element or Component
html: str = render(div(p("Hello")))
# when() — conditional rendering, returns element or None
content = when(user.is_admin, admin_panel)
Component.clone(**overrides) creates a new instance with changed props without mutating the original:
base = Alert(variant=AlertVariant.INFO, content="Default message")
success = base.clone(variant=AlertVariant.SUCCESS, content="Saved!")
Quality & Testing
htmforge is built for production:
238 tests passing · mypy --strict clean · ruff lint + format clean · CI on Python 3.11 / 3.12 / 3.13
- Unit tests — render logic, HTMX attributes, edge cases for all components
- Snapshot tests (21) — HTML regression detection, auto-generated on first run
- Performance benchmarks — 1 000 renders of elements <1s, DataTable <2s
- Framework adapter tests — FastAPI, Flask, Django with graceful skip if not installed
pytest # all tests
pytest -v # verbose
mypy htmforge/ --strict # type check
ruff check htmforge/ # lint
ruff format --check htmforge/ # format check
What htmforge is not
- Not a new framework — sits on top of FastAPI, Flask, or Django
- Not a JavaScript replacement — uses HTMX, not a SPA approach
- Not a template language — pure Python classes and functions
- Not a backend layer — no auth, no ORM, no routing
License
MIT License with the Commons Clause condition.
Free for personal projects, open-source projects, and small businesses. Organizations with annual revenue or funding over USD 1 000 000 or more than 100 employees require a separate commercial license — contact the author.
See LICENSE for the full text.
Contributing
Contributions are welcome! Read CONTRIBUTING.md for setup instructions, coding standards, and the commit convention. The full docs are at mondi04.github.io/htmforge.
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file htmforge-0.3.1.tar.gz.
File metadata
- Download URL: htmforge-0.3.1.tar.gz
- Upload date:
- Size: 2.7 MB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
80021636614202f61b5f0830ae0bcfaa74b51c30e51167cef8c708d9536ba097
|
|
| MD5 |
de770b817b8db05fbaca08d1c93513e5
|
|
| BLAKE2b-256 |
90d3693fe3dc99717c2fc6512f68ecf82a824c8b396a623648a0332453b1a3df
|
File details
Details for the file htmforge-0.3.1-py3-none-any.whl.
File metadata
- Download URL: htmforge-0.3.1-py3-none-any.whl
- Upload date:
- Size: 37.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.0
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
10f42342720cd69bd9d413a5d8c0ef2d0fdb37a2a5f5c985ec1fb2924f8e0b96
|
|
| MD5 |
5e98f7235f074d78da20bcbbebfb4bab
|
|
| BLAKE2b-256 |
169a202e52e6ed9acddedffb50cc0f793abb584eb0866323bc31a98016bc6a77
|