HX is a lightweight Go library for generating HTML programmatically using pure Go — without template engines or languages. HX provides a set of types and functions that represent HTML elements and attributes, and provides rendering and caching functionality.
The basic idea is that elements are just []any, so they can be used like:
d := hx.Div{
hx.H1{"Hello, World!"},
hx.P{"This is a paragraph."},
"Some text outside of any element.",
}
// can also be built up incrementally:
d = append(d, hx.P{"Another paragraph."})Attributes are functions that return attr.Attr, which can be added to elements using builder syntax:
a := hx.A{
attr.Href("/path").Class("link"),
"Click here",
}Elements are rendered by adding them to a container like:
doc := hx.Doc{
Head: hx.Head{
hx.Title{"My Page"},
},
Body: hx.Body{
d, // the div defined above
},
}
// can also append later
doc.Append(a)Finally, rendering is done by calling Render on the container:
// render the entire HTML document to an io.Writer
// for example, os.Stdout:
doc.Render(os.Stdout)go get github.com/shayanderson/hx
Start by creating a document:
// define a document
var doc = hx.Doc{
Attrs: attr.Lang("en"), // set lang attribute on <html>
Head: hx.Head{ // define <head>
hx.Title{"My Page"}, // <title>My Page</title>
},
Body: hx.Body{ // define <body>
// add any elements or supported types
hx.H1{"Welcome to My Page"},
hx.P{"This is a sample paragraph."},
hx.A{attr.Href("https://example.com"), "Click here"},
"Some text outside of any element.",
},
}Then render it to an io.Writer, like using http.ResponseWriter in an HTTP server:
func MyHandler(w http.ResponseWriter, r *http.Request) {
err := doc.Render(w)
if err != nil {
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}Containers hold elements and supported types, providing rendering and caching functionality. The main container is hx.Doc, which represents an entire HTML document. The hx.Container type can be used for HTML partials. Containers have hx.Cache options for caching rendered output.
Example hx.Doc usage as a reusable component:
package layout
// imports...
type Data {
Title string
Description string
}
func Base(data Data, nodes ...any) hx.Doc {
return hx.Doc{
// Cache: hx.Cache{}, // optional caching options
Attrs: attr.Lang("en"),
Head: hx.Head{
hx.Meta{attr.Charset("UTF-8")},
hx.Meta{attr.Name("viewport").Content("width=device-width, initial-scale=1.0")},
hx.Title{data.Title},
hx.Meta{attr.Name("description").Content(data.Description)},
hx.Link{attr.Rel("stylesheet").Href("/static/base.min.css")},
},
Body: hx.Body{
attr.Class("text-slate-100 antialiased bg-slate-950"),
nodes,
},
}
}Example layout.Base usage with a page:
package page
// imports...
func Index() hx.Renderer {
return layout.Base(
layout.Data{
Title: "Page Title",
Description: "Page description.",
},
hx.H1{"Hello, World!"},
hx.P{"This is the home page."},
)
}Then an example of rendering in an HTTP handler:
func IndexHandler(w http.ResponseWriter, r *http.Request) {
if err := page.Index().Render(w); err != nil {
http.Error(w, "internal server error", http.StatusInternalServerError)
}
}An hx.Container can be used for partials:
func Partial() hx.Container {
return hx.Container{
// Cache: hx.Cache{}, // optional caching options
Nodes: hx.Div{
attr.Class("partial"),
hx.P{"This is a partial."},
},
}
}Elements are just []any, so they can include other elements, attributes and many other supported types. For example:
hx.Div{
attr.Class("container"),
hx.H1{"Hello, World!"},
[]string{"Item 1", "Item 2", "Item 3"},
"Some text here.",
}Creating child elements can be done inline or by building them up incrementally:
items := hx.Ul{
hx.Li{"Item 1"},
hx.Li{"Item 2"},
hx.Li{"Item 3"},
}
// or, using append:
items := hx.Ul{}
items = append(items, hx.Li{"Item 1"}, hx.Li{"Item 2"})
items = append(items, hx.Li{"Item 3"})All of the following types can be used as children of elements.
Any hx element type (e.g. hx.Div, hx.P, hx.A, etc.) can be used as a child element.
hx.Div{
hx.H1{"Hello, World!"},
hx.P{"This is a paragraph."},
}string values are rendered as text nodes, with HTML escaping applied.
hx.P{"This is a paragraph."}[]string slices are rendered as multiple text nodes.
hx.Div{
[]string{"Item 1", "Item 2", "Item 3"},
}func() string functions are called to get the string value at render time.
func message() string {
return "Hello from function!"
}
hx.P{message}func() []string functions returning string slices are called to get multiple text nodes at render time.
func items() []string {
return []string{"Item 1", "Item 2", "Item 3"}
}
hx.Div{items}int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, and float64 values are rendered as text nodes.
hx.P{42, 1} // renders as: <p>421</p>
hx.Div{attr.ID("item-", 7)}
// renders as: <div id="item-7"></div>bool values are rendered as text nodes with "true" or "false".
hx.P{true} // renders as: <p>true</p>fmt.Stringer values are rendered using their String() method.
type User struct {
FirstName string
LastName string
}
func (u User) String() string {
return u.FirstName + " " + u.LastName
}
hx.P{
User{FirstName: "Alice", LastName: "Smith"},
} // renders as: <p>Alice Smith</p>func() fmt.Stringer functions are called to get the value at render time.
func getUser(first, last string) fmt.Stringer {
return User{FirstName: first, LastName: last}
}
hx.P{getUser("Bob", "Jones")} // renders as: <p>Bob Jones</p>hx.Raw values are rendered as raw HTML without escaping. This should be used carefully to avoid issues like XSS.
hx.Div{
hx.Raw("<strong>This is bold text.</strong>"),
} // renders as: <div><strong>This is bold text.</strong></div>func() hx.Raw functions are called to get the raw HTML at render time.
func getRaw() hx.Raw {
return hx.Raw("<em>This is italic text.</em>")
}
hx.Div{getRaw} // renders as: <div><em>This is italic text.</em></div>[]any slices can be used to group multiple child nodes together.
els := []any{
hx.H2{"Section Title"},
hx.P{"This is a paragraph in the section."},
}
hx.Div{els}func() any functions returning any values are called to get the value at render time.
func getElement() any {
return hx.P{"This is from a function."}
}
hx.Div{getElement}func() []any functions returning []any slices are called to get multiple child nodes at render time.
func getElements() []any {
return []any{
hx.H2{"Section Title"},
hx.P{"This is a paragraph in the section."},
}
}
hx.Div{getElements}If a type is not supported as a child of an element, an error will be returned during rendering, like "unsupported node type ...".
You can define reusable components as functions or structs that return hx.Renderer, for example:
package components
// imports...
type Button struct {
Label string
Href string
}
func (b Button) Make() hx.Component {
return hx.A{
attr.Class("btn", "btn-primary").Href(b.Href),
b.Label,
}
}Usage example:
hx.Div{
hx.H1{"Buttons example"}
components.Button{
Label: "Click Me",
Href: "/click",
},
}Creating custom elements is simple as creating a component, which satisfies the hx.Component interface:
type Component interface {
Make() Element
}For example, a custom element type:
type MyElement []any
func (e MyElement) Make() Element {
return Element{
Tag: Tag{Name: "my-element"},
Nodes: e,
}
}Usage:
hx.Div{
MyElement{
hx.P{"This is inside my custom element."},
},
}
// renders as:
// <div><my-element><p>This is inside my custom element.</p></my-element></div>When conditionally adding elements or attributes, you can use any type and assign an empty value "" when you want to skip adding an element. For example:
var contactButton any = ""
if !hideContactButton {
contactButton = hx.A{
attr.Href("#contact").Class("btn"),
"Contact",
}
}
return hx.Div{
hx.H1{"Welcome"},
contactButton, // will be skipped if empty
}Attributes are functions that return attr.Attr, which can be added to elements using builder syntax. Various supported types can be used as attribute values. For example:
a := hx.A{
attr.Href("/path").Class("link"),
"Click here",
}Or, using multiple attributes as separate calls:
a := hx.A{
attr.Href("/path"),
attr.Class("link"),
"Click here",
}Most attribute functions support variadic arguments for convenience, like:
div := hx.Div{
attr.ID("div-", 1),
"Content goes here.",
} // renders as: <div id="div-1">Content goes here.</div>The attr.Class function is will add space-separated values for multiple arguments:
div := hx.Div{
attr.Class("container", "main", "theme-dark"),
} // renders as: <div class="container main theme-dark"></div>The attr.AriaHidden and attr.Spellcheck take boolean values:
span := hx.Span{
attr.AriaHidden(true),
} // renders as: <span aria-hidden="true"></span>Some attribute functions take no arguments, for example:
input := hx.Input{
attr.Required(),
} // renders as: <input required>The attr.Aria and attr.Data functions can be used to create custom aria- and data- attributes:
div := hx.Div{
attr.Aria("label", "Close"),
attr.Data("id", 123),
} // renders as: <div aria-label="Close" data-id="123"></div>All of the following types can be used as attribute values.
string values are rendered as string with HTML escaping applied.
hx.Div{attr.ID("my-id")} // renders as: <div id="my-id"></div>int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, and float64 values are rendered as text.
hx.Div{attr.ID("item-", 7)} // renders as: <div id="item-7"></div>bool values are rendered as "true" or "false".
hx.Div{attr.Data("active", true)} // renders as: <div data-active="true"></div>func() string functions are called to get the string value at render time.
func getID() string {
return "dynamic-id"
}
hx.Div{attr.ID(getID)} // renders as: <div id="dynamic-id"></div>Creating custom attributes can be done using the attr.Attr type directly:
myAttr := attr.Attr{
Name: "data-custom",
Value: "customValue",
}
div := hx.Div{myAttr} // renders as: <div data-custom="customValue"></div>Or, no value:
myAttr := attr.Attr{
Name: "my-flag",
NoValue: true,
}
div := hx.Div{myAttr} // renders as: <div my-flag></div>Rendering is done by calling the Render method on a container, like hx.Doc or hx.Container. The Render method takes an io.Writer as an argument and writes the rendered HTML to it. Caching can be configured on containers to improve performance for static or infrequently changing content.
Example rendering to os.Stdout:
err := doc.Render(os.Stdout)
if err != nil {
log.Fatal(err)
}A custom HTML formatter can be used if HTML formatting is desired. For example:
type MyFormatter struct{
renderer hx.Renderer
}
// ...
func (m *MyFormatter) Render(w io.Writer) error {
var buf bytes.Buffer
if err := m.renderer.Render(&buf); err != nil {
return err
}
// implement your own formatting logic:
formattedHTML := formatHTML(buf.String())
_, err := w.Write([]byte(formattedHTML))
return err
}Containers hx.Doc and hx.Container support caching rendered output. Caching can significantly improve performance for static or infrequently changing content by avoiding redundant rendering. Caching is enabled by default, but can be configured using the hx.Cache field on containers or globally.
Example caching configuration on a container:
doc := hx.Doc{
Cache: hx.Cache{
// disable caching for this container, default is false
// overridden by global setting if set to true
Disabled: false,
// prefix to add to cache keys, default is ""
KeyPrefix: "mydoc-",
// time-to-live duration for cached items, 0 means no expiration
// default is 0
// overrides global default TTL if set
TTL: 10 * time.Minute,
},
// other fields...
}
// first render will generate and cache the output
err := doc.Render(os.Stdout)
if err != nil {
// ...
}
// subsequent renders will use the cached output
err = doc.Render(os.Stdout)
// ...Global configuration can be set using the hx.SetDefaultConfig function. Example:
hx.SetDefaultConfig(hx.Config{
// set a custom cache store
Cacher: myCacher,
// disable caching globally, overrides container settings
CacheDisable: false,
// set default TTL for cached items, used if container TTL is 0
CacheTTL: 5 * time.Minute,
})The hx.Config.Cacher field can be set to any type that satisfies the hx.Cacher interface, allowing for custom caching implementations, such as using external caching systems or databases. You can also add features like metrics (hits, misses, evictions, etc.) and logging.
type Cacher interface {
// Get retrieves a cached item by key
// returns the cached item and true if found, otherwise nil and false
Get(string) (*CacheItem, bool)
// Set stores a cache item
Set(*CacheItem)
}HX can be used effectively with Tailwind CSS by leveraging the attr.Class attribute function to add Tailwind utility classes to elements. Additionally, you can configure your IDE for better Tailwind CSS support when using HX.
Example tailwind.config.js:
/** @type {import('tailwindcss').Config} */
module.exports = {
content: ["./**/*.go"],
theme: { extend: {} },
plugins: [],
}Add to settings.json:
{
"tailwindCSS.includeLanguages": {
"go": "html"
},
"tailwindCSS.experimental.classRegex": [
// matches Class("a b") and attr.Class("a", "b c", ...)
["(?:\\b\\w+\\.)?Class\\(([^)]*)\\)", "\"([^\"]+)\""]
]
}