Write HTML templates. Get type-safe Gleam. Like magic. ✨
Ever found yourself writing Lustre views like this? 😩
html.div([attribute.class("card")], [
html.div([attribute.class("card-header")], [
html.h1([attribute.class("title")], [text(user.name)]),
html.span([attribute.class("badge")], [text("Admin")]),
]),
html.div([attribute.class("card-body")], [
html.p([], [text(description)]),
// wait, did I close all the brackets...?
]), // <-- is this right?
]) // <-- or this one?Bracket-counting nightmares. We've all been there. 🤯
Write this instead:
@params(user: User, description: String)
<div class="card">
<div class="card-header">
<h1 class="title">{user.name}</h1>
<span class="badge">Admin</span>
</div>
<div class="card-body">
<p>{description}</p>
</div>
</div>Run gleam run -m ghtml and boom — you get a perfectly formatted, type-safe Gleam module. 🎉
1. Install
gleam add ghtml@12. Create a template
Create src/components/greeting.ghtml:
@params(name: String)
<div class="greeting">
<h1>Hello, {name}!</h1>
</div>3. Generate
gleam run -m ghtml4. Use it
import components/greeting
pub fn view(model: Model) -> Element(Msg) {
greeting.render(model.name)
}That's it. You're done. Go grab a coffee. ☕
Hash-based caching means we only rebuild what changed. Run it a thousand times — if nothing changed, nothing rebuilds.
Change a file. Blink. It's regenerated. Your flow stays unbroken.
{#if}, {#each}, {#case} — all the control flow you need, right in your templates.
{#if user.is_admin}
<span class="badge">Admin</span>
{/if}
{#each items as item}
<li>{item}</li>
{/each}Delete a .ghtml file and we clean up the generated .gleam file automatically. No orphans left behind.
Outside of watch mode, you can manually remove orphaned files:
gleam run -m ghtml -- cleanEvent handlers? We got 'em.
<button @click={on_save}>Save</button>
<input @input={handle_input} />Web components work too. Tags with hyphens automatically use element().
<my-component data={value}>
<slot-content />
</my-component>📦 Imports & Parameters
@import(gleam/int)
@import(app/models.{type User})
@params(
user: User,
count: Int,
on_click: fn() -> msg,
)✨ Interpolation
<!-- Expressions -->
<p>{user.name}</p>
<p>{int.to_string(count)} items</p>
<!-- Literal braces -->
<p>Use {{ and }} for literal braces</p>🔀 Control Flow
<!-- Conditionals -->
{#if show}
<p>Visible!</p>
{:else}
<p>Hidden</p>
{/if}
<!-- Loops -->
{#each items as item, index}
<li>{int.to_string(index)}: {item}</li>
{/each}
<!-- Pattern matching -->
{#case status}
{:Active}
<span class="green">Active</span>
{:Pending}
<span class="yellow">Pending</span>
{/case}🎯 Attributes & Events
<!-- Static attributes -->
<div class="container" id="main">
<!-- Dynamic attributes -->
<input value={model.text} placeholder={hint} />
<!-- Boolean attributes -->
<input disabled required />
<!-- Events -->
<button @click={on_submit}>Submit</button>
<input @input={handle_change} @blur={on_blur} />Input: src/components/user_card.ghtml
@import(gleam/int)
@params(name: String, count: Int)
<div class="card">
<h1>{name}</h1>
<p>{int.to_string(count)} items</p>
</div>Output: src/components/user_card.gleam
// @generated from user_card.ghtml
// @hash abc123...
// DO NOT EDIT - regenerate with: gleam run -m ghtml
import gleam/int
import lustre/attribute
import lustre/element.{type Element, text}
import lustre/element/html
pub fn render(name: String, count: Int) -> Element(msg) {
html.div([attribute.class("card")], [
html.h1([], [text(name)]),
html.p([], [text(int.to_string(count) <> " items")]),
])
}| Command | What it does |
|---|---|
gleam run -m ghtml |
Generate all (skips unchanged) |
gleam run -m ghtml -- force |
Force regenerate everything |
gleam run -m ghtml -- watch |
Watch mode |
gleam run -m ghtml -- clean |
Remove orphans only |
- 📖 Full Documentation — API reference and guides
- 🤝 Contributing — Development setup and guidelines
- 📁 Examples — Working example projects
This tool is built specifically for the Lustre ecosystem. If you're building web apps with Gleam, you're in the right place.
Built with ☕ and too many brackets by @burakcorekci





