Build reusable, type-safe UI components for template-based web apps in Python. PyJinHx combines Pydantic models with Jinja2 templates to give you template discovery, component composition, and JavaScript bundling.
- Automatic Template Discovery: Place templates next to component files—no manual paths
- JavaScript Bundling: Automatically collects and bundles
.jsfiles from component directories - Composability: Nest components easily—works with single components, lists, and dictionaries
- Flexible: Use Python classes for reusable components, HTML syntax for quick page composition
- Type Safety: Pydantic models provide validation and IDE support
pip install pyjinhxThis single example shows the full setup (Python classes + templates) and both ways to render:
- Python-side: instantiate a component class and call
.render(). - Template-side: render an HTML-like string with custom tags via
Renderer.
# components/ui/button.py
from pyjinhx import BaseComponent
class Button(BaseComponent):
id: str
text: str
variant: str = "default"# components/ui/card.py
from pyjinhx import BaseComponent
from components.ui.button import Button
class Card(BaseComponent):
id: str
title: str
action_button: Button# components/ui/page.py
from pyjinhx import BaseComponent
from components.ui.card import Card
class Page(BaseComponent):
id: str
title: str
main_card: Card<!-- components/ui/button.html -->
<button id="{{ id }}" class="btn btn-{{ variant }}">{{ text }}</button><!-- components/ui/card.html -->
<div id="{{ id }}" class="card">
<h2>{{ title }}</h2>
<div class="action">{{ action_button }}</div>
</div><!-- components/ui/page.html -->
<main id="{{ id }}">
<h1>{{ title }}</h1>
{{ main_card }}
</main>from components.ui.button import Button
from components.ui.card import Card
from components.ui.page import Page
page = Page(
id="home",
title="Welcome",
main_card=Card(
id="hero",
title="Get Started",
action_button=Button(id="cta", text="Sign up", variant="primary"),
),
)
html = page.render()from pyjinhx import Renderer
# Set template directory once
Renderer.set_default_environment("./components")
# Use the default renderer with auto_id enabled
html = Renderer.get_default_renderer(auto_id=True).render(
"""
<Page title="Welcome">
<Card title="Get Started">
<Button text="Sign up" variant="primary"/>
</Card>
</Page>
"""
)Template-side rendering supports:
- Type safety for registered classes: if
Button(BaseComponent)exists, its fields are validated when<Button .../>is instantiated. - Generic tags: if there is no registered class, a generic
BaseComponentis used as long as the template file can be found.
- Component-local JS: if a component class
MyWidgethas a sibling filemy-widget.js, it is auto-collected and injected once at the root render level. - Extra JS: pass
js=[...]with file paths; missing files are ignored. - Extra HTML files: pass
html=[...]with file paths; they are rendered and exposed in the template context by filename stem (e.g.extra_content.html→extra_content.htmlwrapper). Missing files raiseFileNotFoundError.
Optional UI components ship in pyjinhx.builtins. Full reference (fields, px- CSS classes, --px-* tokens, template fallback): docs/guide/builtins.md, or the published site under Guide → Built-in UI components.
# components/ui/button.py
from pyjinhx import BaseComponent
class Button(BaseComponent):
id: str
text: str<!-- components/ui/button.html -->
<button
id="{{ id }}"
hx-post="/clicked"
hx-vals='{"button_id": "{{ id }}"}'
hx-target="#click-result"
hx-swap="innerHTML"
>
{{ text }}
</button>from fastapi import FastAPI
from fastapi.responses import HTMLResponse
from components.ui.button import Button
app = FastAPI()
@app.get("/button", response_class=HTMLResponse)
def button() -> str:
return (
Button(id="save-btn", text="Click me").render()
+ '<div id="click-result"></div>'
)
@app.post("/clicked", response_class=HTMLResponse)
def clicked(button_id: str = "unknown") -> str:
return f"Clicked: {button_id}"- The builtins gallery (
tests/integration/app.py) exposesGET /sse/panel-demo: a slow HTMX SSE stream wired to the Beta slot of the demoPanel, so you can confirm hidden slots stay mounted and keep receiving events. Coverage lives intests/integration/test_panel_sse.py(runs a short-lived uvicorn bound to localhost). - Test suite imports are stabilized by adding the project root to
sys.pathintests/conftest.py. This keeps absolute imports likefrom pyjinhx import ...andfrom tests.ui...working across differentpytestimport modes and Python runners. - Optional builtins (
pyjinhx.builtins) ship twenty UI components with sibling templates, CSS, and JS where needed; documentation lives in docs/guide/builtins.md. Importingpyjinhx.builtinsregisters those classes with the global registry like any otherBaseComponentsubclass. - Built-in components live under
site-packages; Jinja’sFileSystemLoaderdoes not load templates outside the configured root. The renderer therefore falls back to reading adjacent template files from disk for classes in thepyjinhx.builtinspackage when normal relative lookup fails, so your app’s loader can stay pointed at your own template directory. - Co-located JS/CSS names: Auto-collection looks for
pascal_case_to_kebab_case(ClassName) + ".js"/".css"next to the class (e.g.TabGroup→tab-group.js,PanelTrigger→panel-trigger.css,Panel→panel.js/panel.css). Using snake_case filenames such astab_group.jswill not be picked up; templates may still resolve via both snake and kebab candidates. - Built-in template slots: Wrappers that accept arbitrary inner markup use the field and Jinja name
content({{ content }}) — e.g.Notification,Popover,PanelTrigger. Shell-style blocks usebody(Modal,Card,Alert,Drawer). Short static chrome useslabel(Badge,Breadcrumb,TabGrouptab titles).