Gopher logo

Tired of complex template languages?

Try HTML components in pure Go.

gomponents are HTML components in pure Go. They render to HTML 5, and make it easy for you to build reusable components. So you can focus on building your app instead of learning yet another templating language.

$ go get github.com/maragudk/gomponents

See gomponents on Github or buy the Official gomponents <marquee> Element 🤯🤩

Video introduction

Into video? See this lightning talk from GopherCon 2021.

Components are just functions

Have a look at this component. If you know HTML, you know what it does. Easy, right?

func Navbar() g.Node {
	return Nav(Class("navbar"),
		Ol(
			Li(A(Href("/"), g.Text("Home"))),
			Li(A(Href("/contact"), g.Text("Contact"))),
			Li(A(Href("/about"), g.Text("About"))),
		),
	)
}

Let's deduplicate a bit.

func Navbar() g.Node {
	return Nav(Class("navbar"),
		Ol(
			NavbarItem("Home", "/"),
			NavbarItem("Contact", "/contact"),
			NavbarItem("About", "/about"),
		),
	)
}

func NavbarItem(name, path string) g.Node {
	return Li(A(Href(path), g.Text(name)))
}

Inline if

Sometimes you only want to show a component based on some condition. Enter If:

func Navbar(loggedIn bool) g.Node {
	return Nav(Class("navbar"),
		Ol(
			NavbarItem("Home", "/"),
			NavbarItem("Contact", "/contact"),
			NavbarItem("About", "/about"),

			g.If(loggedIn,
				NavbarItem("Log out", "/logout"),
			),
		),
	)
}

func NavbarItem(name, path string) g.Node {
	return Li(A(Href(path), g.Text(name)))
}

Map data to components

What if you have data and want to map it to components? No problem.

type NavLink struct {
	Name string
	Path string
}

func Navbar(loggedIn bool, links []NavLink) g.Node {
	return Nav(Class("navbar"),
		Ol(
			g.Group(g.Map(links, func(l NavLink) g.Node {
				return NavbarItem(l.Name, l.Path)
			})),

			g.If(loggedIn,
				NavbarItem("Log out", "/logout"),
			),
		),
	)
}

func NavbarItem(name, path string) g.Node {
	return Li(A(Href(path), g.Text(name)))
}

Styling

Want to apply CSS classes based on state? Use the Classes helper.

type NavLink struct {
	Name string
	Path string
}

func Navbar(loggedIn bool, links []NavLink, currentPath string) g.Node {
	return Nav(Class("navbar"),
		Ol(
			g.Group(g.Map(links, func(l NavLink) g.Node {
				return NavbarItem(l.Name, l.Path, l.Path == currentPath)
			})),

			g.If(loggedIn,
				NavbarItem("Log out", "/logout", false),
			),
		),
	)
}

func NavbarItem(name, path string, active bool) g.Node {
	return Li(A(Href(path), g.Text(name)), c.Classes{
		"navbar-item": true,
		"active":      active,
		"inactive":    !active,
	})
}

Sometimes you just want HTML

Miss those <tags> or need to inject HTML from somewhere else? Use Raw.

type NavLink struct {
	Name string
	Path string
}

func Navbar(loggedIn bool, links []NavLink, currentPath string) g.Node {
	return Nav(Class("navbar"),
		Ol(
			g.Raw(`<span class="logo"><img src="logo.png></span>"`),

			g.Group(g.Map(links, func(l NavLink) g.Node {
				return NavbarItem(l.Name, l.Path, l.Path == currentPath)
			})),

			g.If(loggedIn,
				NavbarItem("Log out", "/logout", false),
			),
		),
	)
}

func NavbarItem(name, path string, active bool) g.Node {
	return Li(A(Href(path), g.Text(name)), c.Classes{
		"navbar-item": true,
		"active":      active,
		"inactive":    !active,
	})
}

It's all just Go

Your editor helps you with auto-completion. It's type-safe. Nice formatting using gofmt. And all common HTML elements and attributes are included!

Get started

$ go get github.com/maragudk/gomponents

A sample app

Sometimes there's nothing like seeing it in action. Try running this complete program and going to localhost:8080 .

package main

import (
	"errors"
	"log"
	"net/http"
	"time"

	g "github.com/maragudk/gomponents"
	c "github.com/maragudk/gomponents/components"
	. "github.com/maragudk/gomponents/html"
)

func main() {
	http.Handle("/", createHandler(indexPage()))
	http.Handle("/contact", createHandler(contactPage()))
	http.Handle("/about", createHandler(aboutPage()))

	if err := http.ListenAndServe("localhost:8081", nil); err != nil && !errors.Is(err, http.ErrServerClosed) {
		log.Println("Error:", err)
	}
}

func createHandler(title string, body g.Node) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
		// Rendering a Node is as simple as calling Render and passing an io.Writer
		_ = Page(title, r.URL.Path, body).Render(w)
	}
}

func indexPage() (string, g.Node) {
	return "Welcome!", Div(
		H1(g.Text("Welcome to this example page")),
		P(g.Text("I hope it will make you happy. 😄 It's using TailwindCSS for styling.")),
	)
}

func contactPage() (string, g.Node) {
	return "Contact", Div(
		H1(g.Text("Contact us")),
		P(g.Text("Just do it.")),
	)
}

func aboutPage() (string, g.Node) {
	return "About", Div(
		H1(g.Text("About this site")),
		P(g.Text("This is a site showing off gomponents.")),
	)
}

func Page(title, path string, body g.Node) g.Node {
	// HTML5 boilerplate document
	return c.HTML5(c.HTML5Props{
		Title:    title,
		Language: "en",
		Head: []g.Node{
			Script(Src("https://cdn.tailwindcss.com?plugins=typography")),
		},
		Body: []g.Node{
			Navbar(path, []PageLink{
				{Path: "/contact", Name: "Contact"},
				{Path: "/about", Name: "About"},
			}),
			Container(
				Prose(body),
				PageFooter(),
			),
		},
	})
}

type PageLink struct {
	Path string
	Name string
}

func Navbar(currentPath string, links []PageLink) g.Node {
	return Nav(Class("bg-gray-700 mb-4"),
		Container(
			Div(Class("flex items-center space-x-4 h-16"),
				NavbarLink("/", "Home", currentPath == "/"),

				// We can Map custom slices to Nodes
				g.Group(g.Map(links, func(l PageLink) g.Node {
					return NavbarLink(l.Path, l.Name, currentPath == l.Path)
				})),
			),
		),
	)
}

// NavbarLink is a link in the Navbar.
func NavbarLink(path, text string, active bool) g.Node {
	return A(Href(path), g.Text(text),
		// Apply CSS classes conditionally
		c.Classes{
			"px-3 py-2 rounded-md text-sm font-medium focus:outline-none focus:text-white focus:bg-gray-700": true,
			"text-white bg-gray-900":                           active,
			"text-gray-300 hover:text-white hover:bg-gray-700": !active,
		},
	)
}

func Container(children ...g.Node) g.Node {
	return Div(Class("max-w-7xl mx-auto px-2 sm:px-6 lg:px-8"), g.Group(children))
}

func Prose(children ...g.Node) g.Node {
	return Div(Class("prose"), g.Group(children))
}

func PageFooter() g.Node {
	return Footer(Class("prose prose-sm prose-indigo"),
		P(
			// We can use string interpolation directly, like fmt.Sprintf.
			g.Textf("Rendered %v. ", time.Now().Format(time.RFC3339)),

			// Conditional inclusion
			g.If(time.Now().Second()%2 == 0, g.Text("It's an even second.")),
			g.If(time.Now().Second()%2 == 1, g.Text("It's an odd second.")),
		),

		P(A(Href("https://www.gomponents.com"), g.Text("gomponents"))),
	)
}

See also the Github repository or the blog post that started it all.

Buy the Official gomponents <marquee> Element here!