fhtml
fhtml — Fluid Hypertext Markup Language

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.

team.fhtmlfhtml
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")
Rendered output

Erin Lindford

Product Engineer

Online

Tom Cook

Director of Operations

Away

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.

card.fhtmlfhtml
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"
index.htmlhtml
<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.

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.

See every rule with examples in the docs →

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.

Template docs

users.fhtmlfhtml
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 →