Write type-safe HTML in .hyper files. Import components directly from Python.
uv add hyperhtmlRequires Python 3.10 or newer.
Create app/pages/Greeting.hyper:
<h1>Hello, World!</h1>
Import and call it:
from app.pages import Greeting
print(Greeting())<h1>Hello, World!</h1>The filename becomes the component name. Hyper compiles it on first import.
Add typed props above ---:
name: str
---
<h1>Hello, {name}!</h1>
print(Greeting(name="Ada"))<h1>Hello, Ada!</h1>Props are keyword-only. Values are escaped by default.
Use component for reusable markup inside the file:
# app/pages/Dashboard.hyper
title: str
---
component Header(*, title: str):
<header><h1>{title}</h1></header>
end
<{Header} title={title} />
The file exports Dashboard and its nested Header:
from app.pages import Dashboard
Dashboard(title="Account")
Dashboard.Header(title="Account")Both calls return:
<header><h1>Account</h1></header>Dashboard("Account") raises TypeError because props are keyword-only.
Place {...} where caller content belongs:
# app/components/Card.hyper
title: str
---
<article>
<h2>{title}</h2>
<main>{...}</main>
</article>
Pass content between component tags:
# app/pages/Confirm.hyper
from app.components import Card
---
<{Card} title="Delete item">
<p>This cannot be undone.</p>
</{Card}>
<article><h2>Delete item</h2><main><p>This cannot be undone.</p></main></article>A file with declarations and no rendered output is a component library:
# app/components/forms.hyper
component Button(*, label: str, **attrs):
<button {**attrs}>{label}</button>
end
component Input(*, name: str, type: str = "text"):
<input {name} {type} />
end
Import its components like a Python module:
from app.components.forms import Button, Input
Button(label="Save", disabled=True)
Input(name="email", type="email")<button disabled>Save</button>
<input name="email" type="email">Python statements and expressions stay Python:
items: list[str]
show_count: bool = True
---
<ul>
for item in items:
<li>{item.upper()}</li>
end
</ul>
if show_count:
<p>{len(items)} items</p>
end
Imports, functions, classes, match, try, with, and async code use the same block form.
Formatting whitespace between attributes does not render:
<{Button}
label="Save"
class={["button", {"active": active}]}
hx-post="/save"
/>
Calling a component joins its chunks. .stream() returns the generated iterator:
html = Feed(posts=posts)
chunks = Feed.stream(posts=posts)from fastapi.responses import StreamingResponse
@app.get("/feed")
def feed():
return StreamingResponse(
Feed.stream(posts=posts),
media_type="text/html",
)Django views can return a component directly:
from django.http import HttpResponse
def home(request):
return HttpResponse(Dashboard(title="Account"))Jinja can discover .hyper files from its template paths:
env.add_extension("hyperhtml.integrations.jinja2.HyperExtension"){{ Dashboard(title="Account") }}See Integrations for Django templates, Jinja slots, Flask, Litestar, and Sanic.
.hyper file |
Python import |
|---|---|
Contains rendered output or --- |
Component named after the file |
| Contains declarations only | Normal module with exported names |
Has an adjacent .py file |
Python file wins |
Implicit file components must live inside a package. Root-level .hyper files can be component libraries.
- JetBrains: syntax highlighting, Python language injection, and compiler diagnostics
- TextMate and VS Code: syntax highlighting