smithery.ai

element-html-builder

Element is a zero dependency library to efficiently generate HTML, without templates in Go

First seen Apr 11, 2026

Installation

$ npx skills add https://smithery.ai

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery.ai · top by installs.

npx skills add https://smithery.ai

Browse all from smithery.ai

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 19,446 B
  • docs SUMMARY.md 118 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 1 installs

SKILL.md

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

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

  1. Zero dependencies
  2. Buffer pools and component caching for super-high traffic situations
  3. Go's formatting naturally follows the HTML tree structure
  4. Deterministic output: attributes render in the order you pass them

Installation

go get -u github.com/rohanthewiz/element

General Strategy

  1. Write the opening tag with attributes
  2. 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")  // "<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.

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"

// 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. Use element.B() or element.NewBuilder() instead.

Simple Elements

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()

Complete Page Example

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()

Building with Multiple Functions

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
}

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")           // <div class="container">
b.PClass("intro", "id", "p1")     // <p class="intro" id="p1">
b.ButtonClass("btn primary")      // <button class="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()
// 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>

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."),
)
// Output: <p>This is <strong>important</strong> information.</p>

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)
    }),
)
// Output: <ul><li>Apple</li><li>Banana</li><li>Cherry</li></ul>

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:

// 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),
)

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
}

// 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:

  • 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) {
    // 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
}

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()
    // 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
    )
}

Anti-Patterns

❌ Forgetting to Terminate Elements

// 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>

❌ Odd Number of Attributes

// 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>

❌ Using T() When You Need R()

// 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"),
)

❌ Creating Elements Without a Builder

// 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()

❌ Not Using Pooling in High-Throughput Handlers

// 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)
    // ...
}

❌ Using String() When Writing to io.Writer

// SUBOPTIMAL: Unnecessary string allocation
w.Write([]byte(b.String()))

// BETTER: Use Bytes() directly
w.Write(b.Bytes())

❌ Caching Dynamic Components

// 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{})

❌ Forgetting to Reset Reused Builders

// 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>"

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:

// 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())
}

Debug Mode

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:

  • 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