بنقرة واحدة
element-html-builder
Element is a zero dependency library to efficiently generate HTML, without templates in Go
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
Element is a zero dependency library to efficiently generate HTML, without templates in Go
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
| name | element-html-builder |
| description | Element is a zero dependency library to efficiently generate HTML, without templates in Go |
Programmatic HTML generation in Go without templates.
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.
go get -u github.com/rohanthewiz/element
All HTML generation flows through a Builder instance. The builder maintains an internal buffer where tags are written immediately upon method calls.
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") // "<div id="container">" is NOW in the buffer
// The element is returned, waiting for termination
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.
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().
import "github.com/rohanthewiz/element"
// Standard creation
b := element.NewBuilder()
// Shorthand (equivalent)
b := element.B()
// From pool (for high-throughput HTTP handlers)
b := element.AcquireBuilder()
defer element.ReleaseBuilder(b)
Deprecated:
element.Vars(),element.V(),b.Funcs(),b.Vars(),b.V()are deprecated. Useelement.B()orelement.NewBuilder()instead.
b := element.B()
// Element with text content
b.P().T("Hello, World!")
// Output: <p>Hello, World!</p>
// Element with children
b.Div().R(
b.P().T("First paragraph"),
b.P().T("Second paragraph"),
)
// Output: <div><p>First paragraph</p><p>Second paragraph</p></div>
// Empty element
b.Div().R()
// Output: <div></div>
// Get the HTML string
html := b.String()
b := element.B()
// b.Html() automatically prepends <!DOCTYPE html> — no separate DOCTYPE needed
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()
func main() {
b := element.B()
b.DivClass("container").R(
b.H2().T("Section 1"),
generateList(b),
)
fmt.Println(b.Pretty())
/* // Output:
<div class="container">
<h2>Section 1</h2>
<ul>
<li>Item 1</li>
<li>Item 2</li>
</ul>
</div>
*/
}
func generateList(b *element.Builder) (x any) { // we don't care about the return
// Everything is rendered in the builder immediately!
b.Ul().R(
b.Li().T("Item 1"),
b.Li().T("Item 2"),
)
return
}
| 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 |
| 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) |
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.
Most elements have a *Class variant where the first argument is the class attribute:
b.DivClass("container") // <div class="container">
b.PClass("intro", "id", "p1") // <p class="intro" id="p1">
b.ButtonClass("btn primary") // <button class="btn primary">
| 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 |
| 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 |
Attributes are passed as alternating key-value string pairs:
b.Div("id", "main", "class", "container", "data-role", "content").R()
// Output: <div id="main" class="container" data-role="content"></div>
b.A("href", "/about", "class", "nav-link").T("About Us")
// Output: <a href="/about" class="nav-link">About Us</a>
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()
// Output: <div class="two" id="main"></div>
Use b.T() to inject text alongside child elements:
b.P().R(
b.T("This is "),
b.Strong().T("important"),
b.T(" information."),
)
// Output: <p>This is <strong>important</strong> information.</p>
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)
}
}
}),
)
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)
}),
)
// Output: <ul><li>Apple</li><li>Banana</li><li>Cherry</li></ul>
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
Implement the Component interface for reusable HTML fragments:
// Define the component
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
}
// Use the component
card := Card{Title: "Hello", Content: "World"}
b.Div().R(
card.Render(b),
)
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
}
// Package level: shared across requests; underlying component renders once
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), // replayed from cache after the first render
)
w.Write(b.Bytes())
}
Notes:
examples/cached_component for a runnable demob.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),
)
}),
),
)
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"),
)
func handler(w http.ResponseWriter, r *http.Request) {
// Acquire builder from pool to reduce GC pressure
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()) // Use Bytes() to avoid string allocation
}
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()
// HtmlPage returns the full HTML string directly
return b.HtmlPage(
"body { font-family: sans-serif; }", // CSS (inner content of <style>)
"<title>My Page</title>", // Head content (raw HTML), excluding <style>
PageBody{Title: "Home"}, // Body component
)
}
// WRONG: Missing termination - closing tag never written
b.Div("id", "container")
b.P().T("Content")
// Output: <div id="container"><p>Content</p> (missing </div>)
// CORRECT: Always terminate with R(), T(), or F()
b.Div("id", "container").R(
b.P().T("Content"),
)
// Output: <div id="container"><p>Content</p></div>
// WRONG: Odd attribute count - "disabled" has no value
b.Button("class", "btn", "disabled").T("Click")
// The last attribute is dropped
// CORRECT: Use empty string for boolean-like attributes
b.Button("class", "btn", "disabled", "").T("Click")
// Output: <button class="btn" disabled="">Click</button>
// OR: Use the attribute name as its value
b.Button("class", "btn", "disabled", "disabled").T("Click")
// Output: <button class="btn" disabled="disabled">Click</button>
// WRONG: T() only accepts text, not child elements
b.Div().T(b.P().T("Content")) // This won't work as expected
// CORRECT: Use R() for child elements
b.Div().R(
b.P().T("Content"),
)
// WRONG: Don't try to create elements directly
// There is no public Element constructor
// CORRECT: Always use a Builder
b := element.B()
b.Div().R()
// SUBOPTIMAL: Creates GC pressure under load
func handler(w http.ResponseWriter, r *http.Request) {
b := element.B() // New allocation every request
// ...
}
// BETTER: Use pooling for high-traffic handlers
func handler(w http.ResponseWriter, r *http.Request) {
b := element.AcquireBuilder()
defer element.ReleaseBuilder(b)
// ...
}
// SUBOPTIMAL: Unnecessary string allocation
w.Write([]byte(b.String()))
// BETTER: Use Bytes() directly
w.Write(b.Bytes())
// WRONG: Cached renders once and reuses the bytes forever --
// this timestamp will be frozen at the first render
var clock = element.Cached(Clock{}) // Clock renders time.Now()
// CORRECT: Only cache components whose output never changes
var footer = element.Cached(Footer{})
// WRONG: Reusing without reset appends to existing content
b := element.B()
b.P().T("First")
html1 := b.String() // "<p>First</p>"
b.P().T("Second")
html2 := b.String() // "<p>First</p><p>Second</p>" - probably not intended
// CORRECT: Reset before reuse
b.Reset()
b.P().T("Second")
html2 := b.String() // "<p>Second</p>"
Textarea() it is TextArea()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()
}
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())
}
Element works well for HTMX partial responses:
// Handler returning an HTMX partial
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())
}
Enable debug mode during development to catch common mistakes:
// Enable debug mode
element.DebugSet()
// Build your HTML...
// Check for issues (returns diagnostic string)
issues := element.DebugShow()
// Clear debug mode
element.DebugClear()
Debug mode detects:
Debug mode notes:
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.| 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() |