README.md
May 15, 2026 ยท View on GitHub
ppx_html is a PPX that lets you write HTML inside of OCaml ๐ช programs. It is
spiritually similar to JSX.
(the type annotations are unnecessary and only for educational purposes.)
{%html|
<div %{centered : Vdom.Attr.t}>
<p>Capybaras are the world's largest living rodent.</p>
<br />
<img style="width: 50%" src=%{image_url : string} />
%{description : Vdom.Node.t}
</div>
|}
is equivalent to:
Vdom.Node.div
~attrs:[ centered ]
[ Vdom.Node.p [ Vdom.Node.text "Capybaras are the world's largest living rodent." ]
; Vdom.Node.br ()
; Vdom.Node.img ~attrs:[ {%css|width: 50%|}; Vdom.Attr.src image_url ] ()
; description
]
To use it in your project, add ppx_html to your jbuild's preprocess field:
(preprocess (pps (ppx_jane ppx_html)))
Auto-formatting should happen by default.
VIM and VS Code should syntax highlight it by default. You can enable Emacs syntax
highlighting by adding (Jane.polymode) to your Emacs config. Eventually we want to make
syntax highlighting always happen by default in emacs.
Node Syntax
ppx_html's syntax is similar to HTML's. To embed OCaml values, use ppx_string's
familiar syntax:
| Syntax | Description |
|---|---|
| `{%html | %{EXPR} |
| `{%html | %{EXPR#Foo} |
| `{%html | *{EXPR} |
| `{%html | ?{EXPR} |
| `{%html | #{EXPR} |
| `{%html | %{"a string"} |
<TAG ATTRS...> INNER </TAG> | TAG must be Vdom.Node.TAG : ?attrs:Vdom.Attr.t list -> Vdom.Node.t list -> Vdom.Node.t |
<TAG ATTRS.../> | TAG must be Vdom.Node.TAG : ?attrs:Vdom.Attr.t list -> unit -> Vdom.Node.t |
<div %{EXPR} > INNER </div> | EXPR must be Attr.t |
<div ?{EXPR} > INNER </div> | EXPR must be Attr.t option |
<div *{EXPR} > INNER </div> | EXPR must be Attr.t list |
<%{TAGEXPR} ATTRS...> INNER </> | Where TAGEXPR : ?attrs:Vdom.Attr.t list -> Vdom.Node.t list -> Vdom.Node.t |
<%{TAGEXPR} ATTRS.../> | Where TAGEXPR : ?attrs:Vdom.Attr.t list -> unit -> Vdom.Node.t |
<Foo.f> INNER </> | Sugar for <%{Foo.f}> INNER </>. |
<Foo.f> INNER </Foo.f> | Alternate syntax for <Foo.f> INNER </>. |
<Foo.f ~foo:%{EXPR}></> | Passes ~foo:EXPR to Foo.f as an OCaml argument (also supports ?optional arguments). |
<Foo.f ~foo></> | Shorthand for <Foo.f ~foo:%{foo}></> (also supports ?optional arguments). |
<Foo.f ~foo:(<div/>)></> | Passes an HTML element to Foo.f as an OCaml argument (also supports ?optional arguments). |
| `{%html | <></> |
Custom OCaml components and function-call syntax
In addition to HTML tags, you can call OCaml functions as if they were tags. Both the existing manual syntax and the new sugary syntax are supported.
Existing manual custom component syntax:
module Custom_typography = struct
let text children = {%html.jsx|<span style="color: #a1a1a1"> *{children} </span>|}
end
{%html.jsx|
<div>
<%{Custom_typography.text}
><strong>#{"Capybara"}</strong>#{" UI "}</>
</div>
|}
Or more sugary syntax for the same call:
{%html.jsx|
<div>
<Custom_typography.text
><strong>#{"Capybara"}</strong>#{" UI "}</>
</div>
|}
You can also pass named and optional OCaml arguments directly in the tag head, and mix them with HTML-style attributes that become Vdom.Attr.t values. The %{}, *{}, and ?{} forms work both for children and for attributes.
{%html.jsx|
<div>
<Custom_typography.text
><strong>#{"Capybara"}</strong>#{" UI "}</><!-- Function with children and attributes. Named args use ~, optional args use ?. --><Button.view
~on_click
~variant:%{Variant.Filled}
~size:%{`Xs}
%{tomato : Vdom.Attr.t}
disabled
>#{" Hello! "}</><!-- Self-closing function with optional arg punning --><Loading_indicator.spinner
?icon
/>
</div>
|}
Which expands to:
Vdom.Node.div
[ Custom_typography.text
[ Vdom.Node.strong [ Vdom.Node.text "Workflow" ]; Vdom.Node.text "UI" ]
; Button.view
~on_click
~variant:Variant.Filled
~size:`Xs
~attrs:[ tomato; Vdom.Attr.disabled ]
[ Vdom.Node.text "Hello!" ]
; Loading_indicator.spinner ?icon ()
]
The Vdom.Attr.t type annotation above is only for illustration.
How to write APIs that support the sugary syntax
To be callable as a tag:
- With children: write a function of type
Vdom.Node.t list -> Vdom.Node.t- Call sites can write
<Foo.f> child1 child2 </>(or</Foo.f>)
- Call sites can write
- Self-closing: write a function of type
unit -> Vdom.Node.t- Call sites can write
<Foo.f />
- Call sites can write
If you want callers to be able to pass attributes, add the magic optional ?attrs : Vdom.Attr.t list argument to your function. Any HTML-style attributes written at the call site will be collected and passed as this list.
For example:
module Components = struct
let button ?(attrs : Vdom.Attr.t list = []) (children : Vdom.Node.t list) =
{%html.jsx|
<button style="background-color: tomato" *{attrs}>
*{children}
</button>
|}
;;
let image ?(attrs : Vdom.Attr.t list = []) () =
{%html.jsx|<img style="width: 50%" *{attrs} />|}
;;
end
Usage:
{%html.jsx|
<div>
<Components.button on_click=%{fun _ -> order_tomato}
>#{" Order Tomato "}</><Components.image src="./images/order-confirmation.png" />
</div>
|}
You can also add named and optional arguments; callers pass them with ~arg:%{expr} or ?arg:%{expr}. Punning is supported: ~foo and ?foo are shorthand for ~foo:%{foo} and ?foo:%{foo}.
module Components = struct
let button ?(icon : Icon.t option) ?(attrs = []) children =
let icon = icon |> Option.map Icon.view in
{%html.jsx|
<button style="background-color: tomato" *{attrs}>
?{icon} *{children}
</button>
|}
;;
end
{%html.jsx|
<Components.button ~icon:%{Heart} on_click=%{fun _ -> order_tomato}
>#{" Order Tomato "}</>
|}
Expands to:
Components.button
~icon:Heart
~attrs:[ Vdom.Attr.on_click (fun _ -> order_tomato) ]
[ Vdom.Node.text "Order Tomato" ]
Additionally, you can pass HTML elements directly as a named argument using the nested ppx_html syntax: ~arg:(<element/>). This is useful for "slot" patterns where a component accepts multiple nested components as arguments. Instead of creating separate variables, you can write the HTML inline:
module Container = struct
let view ?(footer : Vdom.Node.t option) ~(header : Vdom.Node.t) children =
{%html.jsx|
<div class="container">
<header>%{header}</header>
<main>*{children}</main>
?{footer}
</div>
|}
;;
end
{%html.jsx|
<Container.view ~header:(<h1>Capybara!</h1>) ~footer:(<p>Capyright 2026</p>)>
<p>Capybaras are the world's largest living rodent.</p>
</>
|}
Expands to:
Container.view
~header:{%html.jsx|<h1>Capybara!</h1>|}
~footer:{%html.jsx|<p>Capyright 2026</p>|}
[ {%html.jsx|<p>Capybaras are the world's largest living rodent.</p>|} ]
Rules and notes:
- Call sites must include at least one module qualifier (e.g.,
Foo.f); otherwiseppx_htmllooks forVdom.Node.*. - Only named/optional OCaml arguments are supported in tag position; besides children or unit, positional arguments are not supported.
- The magic
?attrsis where HTML attributes from the tag head are collected, e.g.,disabled,on_click=%{...}, etc.
Quick reference for %{}, *{}, and ?{}
Children position:
{%html.jsx|
<>
<div>%{child : Vdom.Node.t}<!-- single --></div>
<div>*{children : Vdom.Node.t list}<!-- many --></div>
<div>?{maybe_child : Vdom.Node.t option}<!-- optional --></div>
</>
|}
Attribute position:
{%html.jsx|
<>
<div %{attr : Vdom.Attr.t}><!-- single --></div>
<div *{attrs : Vdom.Attr.t list}><!-- many --></div>
<div ?{maybe_attr : Vdom.Attr.t option}><!-- optional --></div>
</>
|}
Attribute Syntax
Nodes may have ATTRS as described below:
NAMEthen NAME isVdom.Attr.NAME : Vdom.Attr.t.NAME=VALUEthen NAME is a name inVdom.Attr.NAME : 'a -> Vdom.Attr.tandVALUEis one ofUNQUOTEDLITERAL- treated as a string. There is a heuristic to parse the string into the correct'a."QUOTED_LITERAL"- treated as a string, but allowsppx_stringinterpolation, and will trigger similar heuristics to parse the string. In particularstyle="..."andstyle=...will useppx_css. Additionally,tailwind="..."andtailwind=...will useppx_tailwind.%{EXPR}- arbitrary ocaml expression that should evaluate the the'athat is expected.
key=VALUEwill pass ~key.
Additionally some OCaml keywords that are also attributes (e.g. for) are special cased
to expand to Vdom.Attr.for_.
How can I use tailwind?
To use ppx_tailwind, , we've special cased a "tailwind" attribute <div tailwind="..."></div> behaves like <div %{[%tailwind ".."]}></div>.
How can I use Virtual_dom_svg?
To create virtual_dom_svg nodes instead of virtual_dom_svg, open
Virtual_dom_svg's Html_syntax:
let open Virtual_dom_svg.Html_syntax in
{%html.jsx|
<svg height=%{100.} width=%{100.}>
<circle
cx=%{50.}
cy=%{50.}
r=%{40.}
stroke=%{`Name "black"}
stroke_width=%{3.}
fill=%{`Name "red"}
></circle>
</svg>
|}
Alternatively, you can:
[%html.jsx.Virtual_dom_svg
{|
<svg height=%{100.} width=%{100.}>
<circle
cx=%{50.}
cy=%{50.}
r=%{40.}
stroke=%{`Name "black"}
stroke_width=%{3.}
fill=%{`Name "red"}
></circle>
</svg>
|}]
You can go back to using virtual_dom nodes by opening Virtual_dom.Html_syntax.
Opening Bonsai_web does this for you!
How can I use ppx_html outside of a Javascript context (e.g. to perform server-side rendering of HTML)?
You can use ppx_html_kernel. It is a version of ppx_html without JavaScript
dependencies and without js_of_ocaml assumptions (e.g. it does not attempt to use
ppx_css).
There is an Html_syntax for lib/html in the library ppx_html_lib_html_syntax. To use it, you must:
- Add the library
ppx_html_lib_html_syntaxas a dependency - Add the ppx
ppx_html_kernel(different fromppx_html!) as a dependency to your preprocess field. - After that you can use
ppx_html_kernelby openingppx_html_lib_html_syntax:
open! Core
open Ppx_html_lib_html_syntax.Html_syntax
let hello =
{%html|
<html>
<head>
<style>
body {
background-color: tomato;
}
</style>
</head>
<body>
<div><h1>Hello!!</h1></div>
</body>
</html>
|};
Note that there are some differences. ppx_html special cases style= into using ppx_css while ppx_html_kernel
does not, which means that using nested CSS inside of style tags (e.g. &:hover body {} will not do what you expect
in ppx_html_kernel).