| name | element-html-builder |
| description | Element is a zero dependency library to efficiently generate HTML, without templates in Go |
Element - AI Agent Documentation
Programmatic HTML generation in Go without templates.
Overview
Element is a zero-dependency Go library that generates HTML programmatically by leveraging Go's natural function execution order. Instead of templates, you write Go code that mirrors HTML's tree structure.
Benefits
- You never leave the safety, speed, and familiarity of Go
- Compiles single-pass with the rest of your Go program -- no extra annotations or build steps
- All of Go is available at any point in your code
- Zero dependencies
- Buffer pools and component caching for super-high traffic situations
- Go's formatting naturally follows the HTML tree structure
- Deterministic output: attributes render in the order you pass them
Installation
go get -u github.com/rohanthewiz/element
General Strategy
- Write the opening tag with attributes
- Render the children and closing tag
Core Concepts
The Builder Pattern
All HTML generation flows through a Builder instance. The builder maintains an internal buffer where tags are written immediately upon method calls.
Immediate Tag Writing
Key Insight: When you call a builder method like b.Div(), the opening tag <div> is written immediately to an internal buffer. The returned element must be terminated with R(), T(), or F() to write the closing tag.
b := element.NewBuilder()
b.Div("id", "container")
Go's function argument evaluation order means children are processed (and their tags written) before the parent's R() closes the parent tag. This is what makes nested structures work correctly.
Terminate all tags
Every non-self-closing element must be terminated with R(), T(), or F(). Self-closing elements like Br, Img, Input, Hr, and Meta don't require termination, but to keep things consistent, close these tags with a sole R() or T().
Basic Usage
Creating a Builder
import "github.com/rohanthewiz/element"
b := element.NewBuilder()
b := element.B()
b := element.AcquireBuilder()
defer element.ReleaseBuilder(b)
Deprecated: element.Vars(), element.V(), b.Funcs(), b.Vars(), b.V() are deprecated. Use element.B() or element.NewBuilder() instead.
Simple Elements
b := element.B()
b.P().T("Hello, World!")
b.Div().R(
b.P().T("First paragraph"),
b.P().T("Second paragraph"),
)
b.Div().R()
html := b.String()
Complete Page Example
b := element.B()
b.Html().R(
b.Head().R(
b.Title().T("My Page"),
b.Meta("charset", "utf-8").R(),
),
b.Body().R(
b.DivClass("container").R(
b.H1().T("Welcome"),
b.P().T("Hello from Element!"),
),
),
)
html := b.String()
Building with Multiple Functions
func main() {
b := element.B()
b.DivClass("container").R(
b.H2().T("Section 1"),
generateList(b),
)
fmt.Println(b.Pretty())
}
func generateList(b *element.Builder) (x any) {
b.Ul().R(
b.Li().T("Item 1"),
b.Li().T("Item 2"),
)
return
}
API Reference
Builder Creation
| Function | Description |
|---|
element.NewBuilder() | Create a new builder |
element.B() | Shorthand for NewBuilder() |
element.AcquireBuilder() | Get builder from pool (high-throughput) |
element.ReleaseBuilder(b) | Return builder to pool |
Termination Methods
| Method | Use Case | Example |
|---|
R(children...) | Elements with children or empty | b.Div().R(b.P().T("hi")) |
T(strings...) | Text-only content (most efficient) | b.P().T("Hello") |
F(format, args...) | Formatted text (like fmt.Sprintf) | b.P().F("Count: %d", 42) |
Element Methods
All standard HTML elements are available: Div, Span, P, A, H1-H6, Ul, Ol, Li, Table, Tr, Td, Th, Form, Input, Button, Label, Img, Br, Hr, Meta, Link, Script, Style, and 90+ more.
Class Convenience Methods
Most elements have a *Class variant where the first argument is the class attribute:
b.DivClass("container")
b.PClass("intro", "id", "p1")
b.ButtonClass("btn primary")
Utility Functions
| Function | Description |
|---|
b.T(strings...) | Write raw text directly to buffer |
b.F(format, args...) | Write formatted text directly to buffer |
b.Wrap(func()) | Execute arbitrary Go code in render tree |
element.ForEach(slice, func(item)) | Generic iteration helper |
element.RenderComponents(b, comps...) | Render multiple components |
b.RenderComps(comps...) | Builder method equivalent of above |
element.Cached(comp) | Render-once wrapper for static components |
Output Methods
| Method | Description |
|---|
b.String() | Get HTML as string |
b.Bytes() | Get HTML as []byte (better for HTTP) |
b.Pretty() | Get HTML as indented/formatted string (debug) |
b.Reset() | Clear buffer for reuse |
b.WriteString(s) | Write raw string directly to buffer |
b.WriteBytes(b) | Write raw bytes directly to buffer |
Common Patterns
Attributes as Key-Value Pairs
Attributes are passed as alternating key-value string pairs:
b.Div("id", "main", "class", "container", "data-role", "content").R()
b.A("href", "/about", "class", "nav-link").T("About Us")
Attributes render in the order passed (deterministic output — safe for exact-string tests).
If a key is repeated, the last value wins at the key's first position:
b.Div("class", "one", "id", "main", "class", "two").R()
Mixed Content (Text and Elements)
Use b.T() to inject text alongside child elements:
b.P().R(
b.T("This is "),
b.Strong().T("important"),
b.T(" information."),
)
Conditional Rendering with Wrap()
Wrap() executes arbitrary Go code within the render tree:
b.Div().R(
b.H2().T("Items"),
b.Wrap(func() {
if len(items) == 0 {
b.P().T("No items found")
} else {
for _, item := range items {
b.Li().T(item)
}
}
}),
)
Iteration with ForEach()
ForEach is a generic helper for slices:
items := []string{"Apple", "Banana", "Cherry"}
b.Ul().R(
element.ForEach(items, func(item string) {
b.Li().T(item)
}),
)
Standard Components
Import the components sub-package:
import "github.com/rohanthewiz/element/components"
| Component | Description |
|---|
Alert | Styled notification banner (info/success/warning/error), optional dismiss button |
Breadcrumb | Navigation breadcrumb trail with configurable separator |
Card | Container with optional header, body (text or component), and footer |
DefinitionList | <dl> term/definition pairs |
FormField | Label + input + optional help text / error message |
Nav | Navigation bar with brand and list of links; marks active item |
Pagination | Page navigation with prev/next/first/last and page number links |
Table | Table with headers, 2D row data, optional striped/bordered styling |
Source: element/components
Custom Components
Implement the Component interface for reusable HTML fragments:
type Card struct {
Title string
Content string
}
func (c Card) Render(b *element.Builder) (x any) {
b.DivClass("card").R(
b.H3().T(c.Title),
b.P().T(c.Content),
)
return
}
card := Card{Title: "Hello", Content: "World"}
b.Div().R(
card.Render(b),
)
Component Caching
Wrap a static component with element.Cached to render it once and replay
the cached bytes on every subsequent render (~20x faster, single allocation).
Ideal for nav bars, footers, and other fragments that never change:
type Footer struct{}
func (f Footer) Render(b *element.Builder) (x any) {
b.FooterClass("footer").R(
b.P().T("Built with Element"),
)
return
}
var cachedFooter = element.Cached(Footer{})
func handler(w http.ResponseWriter, r *http.Request) {
b := element.AcquireBuilder()
defer element.ReleaseBuilder(b)
b.Body().R(
b.H1().T("Fresh dynamic content"),
cachedFooter.Render(b),
)
w.Write(b.Bytes())
}
Notes:
- Safe for concurrent use
- In debug mode the cache is bypassed so concern tracking still works
- No invalidation — if the output can change, don't cache it
- See
examples/cached_component for a runnable demo
Building Tables
b.Table().R(
b.THead().R(
b.Tr().R(
b.Th().T("Name"),
b.Th().T("Value"),
),
),
b.TBody().R(
element.ForEach(rows, func(row DataRow) {
b.Tr().R(
b.Td().T(row.Name),
b.Td().T(row.Value),
)
}),
),
)
Forms
b.Form("method", "post", "action", "/submit").R(
b.Label("for", "email").T("Email:"),
b.Input("type", "email", "id", "email", "name", "email").R(),
b.Br().R(),
b.Label("for", "password").T("Password:"),
b.Input("type", "password", "id", "password", "name", "password").R(),
b.Br().R(),
b.ButtonClass("btn", "type", "submit").T("Submit"),
)
HTTP Handler with Pooling
func handler(w http.ResponseWriter, r *http.Request) {
b := element.AcquireBuilder()
defer element.ReleaseBuilder(b)
b.Html().R(
b.Body().R(
b.H1().T("Hello"),
),
)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write(b.Bytes())
}
Full Page with HtmlPage Helper
type PageBody struct {
Title string
}
func (pb PageBody) Render(b *element.Builder) (x any) {
b.H1().T(pb.Title)
b.P().T("Welcome to the page")
return
}
func generatePage() string {
b := element.B()
return b.HtmlPage(
"body { font-family: sans-serif; }",
"<title>My Page</title>",
PageBody{Title: "Home"},
)
}
Anti-Patterns
❌ Forgetting to Terminate Elements
b.Div("id", "container")
b.P().T("Content")
b.Div("id", "container").R(
b.P().T("Content"),
)
❌ Odd Number of Attributes
b.Button("class", "btn", "disabled").T("Click")
b.Button("class", "btn", "disabled", "").T("Click")
b.Button("class", "btn", "disabled", "disabled").T("Click")
❌ Using T() When You Need R()
b.Div().T(b.P().T("Content"))
b.Div().R(
b.P().T("Content"),
)
❌ Creating Elements Without a Builder
b := element.B()
b.Div().R()
❌ Not Using Pooling in High-Throughput Handlers
func handler(w http.ResponseWriter, r *http.Request) {
b := element.B()
}
func handler(w http.ResponseWriter, r *http.Request) {
b := element.AcquireBuilder()
defer element.ReleaseBuilder(b)
}
❌ Using String() When Writing to io.Writer
w.Write([]byte(b.String()))
w.Write(b.Bytes())
❌ Caching Dynamic Components
var clock = element.Cached(Clock{})
var footer = element.Cached(Footer{})
❌ Forgetting to Reset Reused Builders
b := element.B()
b.P().T("First")
html1 := b.String()
b.P().T("Second")
html2 := b.String()
b.Reset()
b.P().T("Second")
html2 := b.String()
Note
- Builder has no method
Textarea() it is TextArea()
Integration Notes
With github.com/rohanthewiz/rweb
Element integrates naturally with the rweb web framework:
import (
"github.com/rohanthewiz/element"
"github.com/rohanthewiz/rweb"
)
func main() {
server := rweb.NewServer(rweb.ServerOptions{
Address: "localhost:8080",
})
server.Get("/", func(ctx *rweb.Context) error {
b := element.AcquireBuilder()
defer element.ReleaseBuilder(b)
b.Html().R(
b.Head().R(
b.Title().T("Home"),
),
b.Body().R(
b.H1().T("Welcome to RWeb + Element"),
),
)
return ctx.WriteHTML(b.String())
})
server.Run()
}
With Standard net/http
func handler(w http.ResponseWriter, r *http.Request) {
b := element.AcquireBuilder()
defer element.ReleaseBuilder(b)
b.Html().R(
b.Body().R(
b.H1().T("Hello"),
),
)
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.Write(b.Bytes())
}
With HTMX
Element works well for HTMX partial responses:
func itemsHandler(w http.ResponseWriter, r *http.Request) {
b := element.AcquireBuilder()
defer element.ReleaseBuilder(b)
items := getItems()
b.Ul("id", "item-list").R(
element.ForEach(items, func(item Item) {
b.Li().R(
b.Span().T(item.Name),
b.Button(
"hx-delete", fmt.Sprintf("/items/%d", item.ID),
"hx-target", "closest li",
"hx-swap", "outerHTML",
).T("Delete"),
)
}),
)
w.Write(b.Bytes())
}
Debug Mode
Enable debug mode during development to catch common mistakes:
element.DebugSet()
issues := element.DebugShow()
element.DebugClear()
Debug mode detects:
- Unclosed tags
- Odd number of attributes
- Children on self-closing elements
- Unwrapped text content
Debug mode notes:
- Elements get a
data-ele-id attribute and caller file:line info is captured
(stack walks), so debug mode is noticeably slower — enable it during
development only. In normal mode this overhead is skipped entirely.
element.Cached components bypass their cache in debug mode so concern
tracking still works.
- Debug mode is safe to use with concurrent rendering.
Quick Reference
| Task | Code |
|---|
| Create builder | b := element.B() |
| Pooled builder | b := element.AcquireBuilder() |
| Element with children | b.Div().R(children...) |
| Element with text | b.P().T("text") |
| Formatted text | b.P().F("Count: %d", n) |
| Add class easily | b.DivClass("name") |
| Multiple attributes | b.A("href", "/", "class", "link") |
| Raw text in tree | b.T("some text") |
| Conditional logic | b.Wrap(func() { if x { ... } }) |
| Iterate slice | element.ForEach(items, func(i T) { ... }) |
| Render component | comp.Render(b) |
| Multiple components | element.RenderComponents(b, c1, c2) |
| Cache static component | cached := element.Cached(comp) |
| Get HTML string | b.String() |
| Get HTML bytes | b.Bytes() |
| Pretty-print HTML | b.Pretty() |
| Reset builder | b.Reset() |
Links