ghtml logo

ghtml

Write HTML templates. Get type-safe Gleam. Like magic. ✨

test Package Version Hex Docs License

ghtml demo


The Problem

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. 🀯

The Solution

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. πŸŽ‰


Quick Start

1. Install

gleam add ghtml@1

2. Create a template

Create src/components/greeting.ghtml:

@params(name: String)

<div class="greeting">
  <h1>Hello, {name}!</h1>
</div>

3. Generate

gleam run -m ghtml

4. 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. β˜•


Features

⚑ Blazing Fast

Hash-based caching means we only rebuild what changed. Run it a thousand times β€” if nothing changed, nothing rebuilds.

Hash-based caching demo

πŸ‘€ Watch Mode

Change a file. Blink. It’s regenerated. Your flow stays unbroken.

Watch mode demo

🎯 Control Flow

{#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}

🧹 Auto Cleanup

Delete a .ghtml file and we clean up the generated .gleam file automatically. No orphans left behind.

Auto cleanup demo

Outside of watch mode, you can manually remove orphaned files:

gleam run -m ghtml -- clean

🎨 Events

Event handlers? We got ’em.

<button @click={on_save}>Save</button>
<input @input={handle_input} />

πŸ”§ Custom Elements

Web components work too. Tags with hyphens automatically use element().

<my-component data={value}>
  <slot-content />
</my-component>

Template Syntax

Control flow syntax demo

πŸ“¦ 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} />

Example

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")]),
  ])
}

Commands

CommandWhat it does
gleam run -m ghtmlGenerate all (skips unchanged)
gleam run -m ghtml -- forceForce regenerate everything
gleam run -m ghtml -- watchWatch mode
gleam run -m ghtml -- cleanRemove orphans only

Documentation


Made for Lustre πŸ’–

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

✨ Search Document