
A component-based markup language that compiles 1:1 to HTML. Real components, slots, loops, and includes — written in whitespace, with your Tailwind classes copied through byte-for-byte. No closing tags, no angle brackets.
Components, not string soup
Define once. Compose anywhere.
Declare a block with
def, give it parameters and a
children slot, then reuse it with
+call. The panel below is this page rendering that exact code.
def badge(tone label)
span shrink-0 rounded-full px-2.5 py-0.5 text-xs font-medium {tone} "{label}"
def card(name role)
. flex items-center gap-4 rounded-2xl border border-slate-200 p-4
. size-11 shrink-0 rounded-full bg-gradient-to-br from-indigo-500 to-fuchsia-500
. min-w-0 flex-1
p truncate font-semibold text-slate-900 "{name}"
p truncate text-sm text-slate-500 "{role}"
children
+card(name="Erin Lindford" role="Product Engineer")
+badge(tone="bg-green-100 text-green-700" label="Online")
+card(name="Tom Cook" role="Director of Operations")
+badge(tone="bg-slate-100 text-slate-500" label="Away")Erin Lindford
Product Engineer
Tom Cook
Director of Operations
Same
def works statically or compiles to a
(data, ctx) => string ES module —
see components →
One line per element. Real HTML out.
Indentation nests; bare tokens after the tag are the class list, copied to the output byte-for-byte. Nothing magic happens to your markup.
div flex items-center gap-4 rounded-xl bg-white p-6 shadow-md
img(src=/img/ava.jpg alt="Erin's avatar") size-12 rounded-full
.
p text-lg font-semibold text-gray-900 "Erin Lindford"
p text-gray-500 "Product Engineer"
button ml-auto rounded-full px-4 py-1 text-sm "Message"<div class="flex items-center gap-4 rounded-xl bg-white p-6 shadow-md">
<img src="/img/ava.jpg" alt="Erin's avatar" class="size-12 rounded-full">
<div>
<p class="text-lg font-semibold text-gray-900">Erin Lindford</p>
<p class="text-gray-500">Product Engineer</p>
</div>
<button class="ml-auto rounded-full px-4 py-1 text-sm">Message</button>
</div>Every token after the tag is a class, verbatim. Read the full syntax →
A whole toolchain, not a preprocessor
The language is small; the surface around it is complete.
Components & slots
def blocks with parameters, a
children slot, and
+call instantiation.
Template layer
Interpolation,
if/
for/
empty over a small closed expression language.
Includes
Split a page across files. Shared defs, one namespace, no import ceremony.
Tooling
A canonical formatter, a reverse
html2fhtml converter, and a language server.
JavaScript & WASM
@fhtml/core for Node, browsers, and the edge — with Express and Hono adapters, plus a Vite plugin.
Tailwind-native
Classes are plain tokens, so
@source scans
.fhtml files directly.
The whole language, in 30 seconds
Line shape:
tag(attrs) #id classes… "text" — everything after the tag is optional.
Nesting
Indentation nests — Python's rules exactly. No closing tags, ever.
Classes
Bare tokens after the tag are classes, copied verbatim. The compiler never looks inside one.
Attributes
Live in parens, butted against the tag —
a(href=/about target=_blank).
Div & id
A lone
. is a div;
#id sets the id.
Text
Quoted text is HTML-escaped;
| starts a multi-line text block.
Inline chains
One inline child with
> —
li > a(href=/docs) "Docs".
Raw HTML
A line starting with
< is raw passthrough — the escape hatch.
Extras
Trailing
\ continues a line;
// comments;
doctype for the DOCTYPE.
Loop and branch over your data
Interpolation, conditionals, and loops render against JSON — over a
small closed expression language, not arbitrary JavaScript. Render
statically with the Rust binary, or compile to a
(data, ctx) => string ES module with
--target=js — byte-identical to the Rust renderer.
def badge(text)
span text-xs text-indigo-400 "{text}"
ul divide-y
for user in users
li flex justify-between py-2
span "{user.name}"
if user.admin
+badge(text="admin")
empty
li text-slate-500 "No users yet."- round-trip DOM fidelity
- 48/48
- html → fhtml → identical DOM
- dependencies in the compiler
- 0
- one Rust binary · cargo install
- fewer tokens than pretty HTML
- −14%
- up to −20% on SVG-light markup
Models write it reliably, too: across four LLMs on one corpus, 90.6% of generated fhtml compiled, versus 44.3% for Pug. Generation benchmark →