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.


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 — 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:

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

htmforge-0.3.1.tar.gz (2.7 MB 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.1-py3-none-any.whl (37.3 kB view details)

Uploaded Python 3

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

Hashes for htmforge-0.3.1.tar.gz
Algorithm Hash digest
SHA256 80021636614202f61b5f0830ae0bcfaa74b51c30e51167cef8c708d9536ba097
MD5 de770b817b8db05fbaca08d1c93513e5
BLAKE2b-256 90d3693fe3dc99717c2fc6512f68ecf82a824c8b396a623648a0332453b1a3df

See more details on using hashes here.

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

Hashes for htmforge-0.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 10f42342720cd69bd9d413a5d8c0ef2d0fdb37a2a5f5c985ec1fb2924f8e0b96
MD5 5e98f7235f074d78da20bcbbebfb4bab
BLAKE2b-256 169a202e52e6ed9acddedffb50cc0f793abb584eb0866323bc31a98016bc6a77

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