gomponents is a Go library that enables building HTML components using pure Go code. Instead of using traditional HTML templates, developers write HTML as Go functions that compile to type-safe, performant HTML5 output. This approach leverages Go's type system, IDE support, and debugging capabilities while avoiding template language complexity.
The fundamental building block is the Node interface:
type Node interface {
Render(w io.Writer) error
}Everything in gomponents implements this interface - elements, attributes, text, and components.
- ElementType: Regular HTML elements (div, span, etc.) and text nodes
- AttributeType: HTML attributes (class, href, etc.)
The library automatically handles proper placement during rendering.
go get maragu.dev/gomponentsContrary to common idiomatic Go, dot imports are the recommended approach for gomponents as they make the code read like a DSL for HTML:
import (
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
. "maragu.dev/gomponents/components"
)For those who prefer avoiding dot imports, use single-letter aliases:
import (
g "maragu.dev/gomponents"
h "maragu.dev/gomponents/html"
c "maragu.dev/gomponents/components"
ghttp "maragu.dev/gomponents/http"
)Core interfaces and helper functions:
NodeinterfaceEl(name string, children ...Node)- create custom elementsAttr(name string, value ...string)- create custom attributesText(string)- HTML-escaped textTextf(format string, args...)- formatted escaped textRaw(string)- unescaped HTMLRawf(format string, args...)- formatted unescaped HTMLGroup([]Node)- group multiple nodesMap[T]([]T, func(T) Node)- transform slices to nodesIf(condition bool, node Node)- conditional renderingIff(condition bool, func() Node)- lazy conditional rendering
All HTML5 elements and attributes as Go functions:
- Elements:
Div(),Span(),A(),H1(), etc. - Attributes:
Class(),ID(),Href(),Style(), etc. - Special:
Doctype()for HTML5 doctype declaration
Higher-level components:
HTML5(HTML5Props)- complete HTML5 document structureClasses- dynamic class management map
HTTP handler integration:
Handlertype - returns (Node, error)Adapt()- converts Handler to http.HandlerFunc
import (
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
// <div class="container">Hello, World!</div>
Div(Class("container"), Text("Hello, World!"))import (
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
// <nav><a href="/">Home</a><a href="/about">About</a></nav>
Nav(
A(Href("/"), Text("Home")),
A(Href("/about"), Text("About"))
)import (
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/components"
. "maragu.dev/gomponents/html"
)
func Page() Node {
return HTML5(HTML5Props{
Title: "My Page",
Language: "en",
Head: []Node{
Meta(Name("author"), Content("John Doe")),
},
Body: []Node{
H1(Text("Welcome")),
P(Text("This is my page")),
},
})
}Create reusable components as functions:
import (
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func Card(title, content string) Node {
return Div(Class("card"),
H2(Class("card-title"), Text(title)),
P(Class("card-content"), Text(content)),
)
}import (
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func UserList(users []User) Node {
return Ul(
Map(users, func(u User) Node {
return Li(Text(u.Name))
}),
)
}import (
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func NavBar(isLoggedIn bool, username string) Node {
return Nav(
A(Href("/"), Text("Home")),
If(isLoggedIn,
Span(Text("Welcome, " + username))),
If(!isLoggedIn,
A(Href("/login"), Text("Login"))),
)
}import (
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/components"
. "maragu.dev/gomponents/html"
)
Div(
Classes{
"active": isActive,
"disabled": isDisabled,
"primary": isPrimary,
},
Text("Dynamic styling"),
)Some HTML names conflict in Go. The library provides both variants:
cite:Cite()(element) /CiteAttr()(attribute)data:DataEl()(element) /Data()(attribute)form:Form()(element) /FormAttr()(attribute)label:Label()(element) /LabelAttr()(attribute)style:StyleEl()(element) /Style()(attribute)title:TitleEl()(element) /Title()(attribute)
Deprecated aliases (CiteEl, DataAttr, FormEl, LabelEl, StyleAttr, TitleAttr) exist for backwards compatibility but should not be used in new code.
Self-closing elements (br, img, input, etc.) are handled automatically. Child nodes that aren't attributes are ignored:
// Correct: <img src="pic.jpg" alt="Picture">
Img(Src("pic.jpg"), Alt("Picture"))
// Text("ignored") won't render for void elements
Img(Src("pic.jpg"), Text("ignored"))import (
"net/http"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
ghttp "maragu.dev/gomponents/http"
)
func HomeHandler(w http.ResponseWriter, r *http.Request) (Node, error) {
return Page("Welcome!"), nil
}
// In main:
http.HandleFunc("/", ghttp.Adapt(HomeHandler))import (
"net/http"
. "maragu.dev/gomponents"
ghttp "maragu.dev/gomponents/http"
)
type HTTPError struct {
Code int
Message string
}
func (e HTTPError) Error() string { return e.Message }
func (e HTTPError) StatusCode() int { return e.Code }
func Handler(w http.ResponseWriter, r *http.Request) (Node, error) {
if unauthorized {
return ErrorPage(), HTTPError{Code: 401, Message: "Unauthorized"}
}
return SuccessPage(), nil
}Build complex UIs from simple, reusable components:
import (
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/components"
. "maragu.dev/gomponents/html"
)
func Layout(title string, content Node) Node {
return HTML5(HTML5Props{
Title: title,
Body: []Node{
Header(),
Main(content),
Footer(),
},
})
}Leverage Go's type system for compile-time guarantees:
import (
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
type ButtonVariant string
const (
ButtonPrimary ButtonVariant = "btn-primary"
ButtonSecondary ButtonVariant = "btn-secondary"
)
func Button(variant ButtonVariant, text string) Node {
return Button(Class(string(variant)), Type("button"), Text(text))
}Nodes render directly to io.Writer for efficiency:
import (
"net/http"
)
// Efficient - streams directly to response
func (h Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
node := BuildPage()
node.Render(w)
}Components are pure functions, making testing straightforward:
import (
"bytes"
"testing"
)
func TestButton(t *testing.T) {
btn := Button("Click me")
var buf bytes.Buffer
btn.Render(&buf)
expected := `<button>Click me</button>`
if buf.String() != expected {
t.Errorf("got %q, want %q", buf.String(), expected)
}
}import (
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func LoginForm() Node {
return Form(Method("post"), Action("/login"),
Label(For("email"), Text("Email:")),
Input(Type("email"), ID("email"), Name("email"), Required()),
Label(For("password"), Text("Password:")),
Input(Type("password"), ID("password"), Name("password"), Required()),
Button(Type("submit"), Text("Login")),
)
}import (
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func DataTable(headers []string, rows [][]string) Node {
return Table(
Thead(
Tr(Map(headers, func(h string) Node {
return Th(Text(h))
})),
),
Tbody(
Map(rows, func(row []string) Node {
return Tr(Map(row, func(cell string) Node {
return Td(Text(cell))
}))
}),
),
)
}import (
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
func NavMenu(items []MenuItem) Node {
return Nav(
Ul(Class("nav-menu"),
Map(items, func(item MenuItem) Node {
return Li(
A(Href(item.URL), Text(item.Label)),
)
}),
),
)
}Works seamlessly with Tailwind, Bootstrap, etc.:
import (
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
// Tailwind CSS
Div(Class("flex items-center justify-between p-4 bg-blue-500"))
// Bootstrap
Div(Class("container-fluid"),
Div(Class("row"),
Div(Class("col-md-6"), Text("Column 1")),
Div(Class("col-md-6"), Text("Column 2")),
),
)Include scripts and handle interactions:
import (
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
Button(
Class("interactive-btn"),
ID("myButton"),
Text("Click me"),
)
Script(Raw(`
document.getElementById('myButton').addEventListener('click', () => {
alert('Clicked!');
});
`))For web components or non-standard elements:
import (
. "maragu.dev/gomponents"
)
// <my-component attr="value">Content</my-component>
El("my-component",
Attr("attr", "value"),
Text("Content"),
)All nodes implement String() for debugging:
import (
"fmt"
. "maragu.dev/gomponents"
. "maragu.dev/gomponents/html"
)
node := Div(Class("test"), Text("Hello"))
fmt.Println(node) // <div class="test">Hello</div>Test component output:
import (
"bytes"
)
var buf bytes.Buffer
err := node.Render(&buf)
html := buf.String()- Direct Rendering: Nodes render directly to io.Writer without intermediate string allocation
- No Reflection: Pure function calls, no runtime reflection overhead
- Compile-Time Safety: Errors caught at compile time, not runtime
- Zero Dependencies: Core library has no external dependencies
- Nil Nodes: Nil nodes are safely ignored during rendering
- Attribute Order: Attributes render in the order they're specified
- Escaping: Use Text() for escaped content, Raw() for unescaped HTML
- Void Elements: Children (except attributes) are ignored for void elements
gomponents provides a type-safe, performant way to generate HTML in Go applications. It's particularly well-suited for:
- Server-side rendered web applications
- API servers that return HTML
- Static site generators
- Email template generation
- Any scenario where you need programmatic HTML generation with Go's type safety
The library's philosophy emphasizes simplicity, type safety, and Go idioms over template languages, making it an excellent choice for Go developers who prefer staying within the Go ecosystem.