Skip to main content

Type-safe, composable UI components for Python — server-side rendered, HTMX-first, 20+ pre-built components.

Project description

htmforge

PyPI version Python 3.11+ License: MIT + Commons Clause mypy strict CI Docs

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.


What you can build with htmforge

A full user admin panel — live search, modals, forms with validation, pagination, toasts — in pure Python, no templates:

📁 examples/admin-panel/ — see the full working demo, clone it, run it in 30 seconds.

# A complete, interactive page in ~20 lines of Python
class UsersPage(BaseAdminPage):
    users: list[dict]
    total: int
    page: int

    def _body_content(self):
        return [
            SearchInput(name="q", hx_get="/users/search", hx_target="#user-table"),
            DataTable(
                id="user-table",
                columns=[ColumnDef("name", "Name"), ColumnDef("email", "Email"), ColumnDef("role", "Role")],
                rows=self.users,
            ),
            Pagination(current=self.page, total_pages=ceil(self.total / 5), hx_target="#user-table"),
        ]

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()

Compared to the alternatives:

htmforge Jinja2 templates dominate htpy
Type-safe props ✅ Pydantic v2
Validated on assignment
XSS protection ✅ built-in ⚠️ manual ⚠️ partial
HTMX typed enums
Pre-built components ✅ 20+
Framework adapters
py.typed / mypy strict ⚠️ partial
  • Type-safe props via Pydantic v2 — validated on construction and assignment
  • XSS protection built-in — markupsafe escapes 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 adaptersto_fastapi(), to_flask(), to_django() out of the box
  • 2 dependencies — only pydantic and markupsafe; 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:

240 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

htmforge-0.3.3.tar.gz (83.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

htmforge-0.3.3-py3-none-any.whl (38.0 kB view details)

Uploaded Python 3

File details

Details for the file htmforge-0.3.3.tar.gz.

File metadata

  • Download URL: htmforge-0.3.3.tar.gz
  • Upload date:
  • Size: 83.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.0

File hashes

Hashes for htmforge-0.3.3.tar.gz
Algorithm Hash digest
SHA256 3a11663234eb3d4d509bebd427e30ca66863f3e1ada7780fa5e82356892cfc35
MD5 d9c049bfd08e0ed4082bad31d5705067
BLAKE2b-256 3a2a872c3d74a522360c8b3bb64332374aac02c188285afc0b4a5887355c74af

See more details on using hashes here.

File details

Details for the file htmforge-0.3.3-py3-none-any.whl.

File metadata

  • Download URL: htmforge-0.3.3-py3-none-any.whl
  • Upload date:
  • Size: 38.0 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/6.2.0 CPython/3.11.0

File hashes

Hashes for htmforge-0.3.3-py3-none-any.whl
Algorithm Hash digest
SHA256 52ecf849a6d5c176f81ebc034624f0e464c355fbf07995d7c311a40bcb16bc78
MD5 1da4f0557bd473d0042bfe308b2b077c
BLAKE2b-256 d764931df5ef65167126e5a7388d6cc48795a42e8c05d08b24d1e8b10c6a1eed

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page