Type‑safe HTML for Go. Server‑first, hypermedia by default, htmx as an optional extension. Compile‑time validation, zero hydration, no client runtime.
Plain generates HTML using pure Go functions instead of template engines. Each element is a typed function with compile‑time validation and IDE autocomplete. You build pages from links and forms, keep state on the server, and add interactivity progressively (e.g., with htmx) — no SSR/hydration/SPA machinery.
Template engines are runtime: Most Go HTML solutions parse templates at runtime, introduce string-based errors, and lack compile-time guarantees about HTML structure.
Struct-based builders are verbose: Existing HTML builders using structs require repetitive field assignments and don't provide intuitive composition patterns.
Missing type safety: HTML attributes and structure errors only surface at runtime or in browsers, not during development.
Plain solves these problems by providing compile-time HTML generation with function composition that feels natural in Go.
div := Div(
Class("container"),
H1(T("Hello"), Class("title")),
P(T("World"), Class("content")),
)
Each HTML element has its own option types. Input elements only accept input-specific attributes:
// This compiles and works
input := Input(
InputType("email"),
InputName("email"),
Required(),
Placeholder("Enter email"),
)
// This fails at compile time - Href is not valid for Input
input := Input(Href("/invalid")) // Compile error
HTML generation happens through method dispatch resolved at compile time. No reflection, no runtime parsing:
component := Div(Class("test"), T("Hello"))
html := Render(component) // Pure string building
- Autocomplete: Functions and options show up in IDE completion
- Go to definition: Jump directly to tag implementations
- Refactoring: Rename functions across your entire codebase
- Type checking: Invalid attribute combinations fail at compile time
Each HTML element lives in its own file (tag_div.go
, tag_input.go
, etc.). This makes the codebase:
- Easy to understand and contribute to
- Simple to extend with new elements
- Clear about what's supported
Components are just Go values. Test them like any other Go code:
func TestButton(t *testing.T) {
btn := Button(
ButtonType("submit"),
T("Click me"),
Class("btn"),
)
html := Render(btn)
if !strings.Contains(html, `type="submit"`) {
t.Error("Missing type attribute")
}
}
Build reusable components by composing smaller ones:
func Card(title, content string) Node {
return Div(
Class("card"),
H2(Text(title), Class("card-title")),
P(Text(content), Class("card-content")),
)
}
func Page() Node {
return Div(
Class("container"),
Card("Welcome", "Get started with Plain"),
Card("Features", "Type-safe HTML in Go"),
)
}
go get github.com/plainkit/html
package main
import (
"fmt"
. "github.com/plainkit/html"
)
func main() {
page := Html(
Lang("en"),
Head(
HeadTitle(T("My Page")),
Meta(Charset("UTF-8")),
HeadStyle(T(".intro { color: blue; }")),
),
Body(
H1(T("Hello, World!")),
P(T("Built with Plain"), Class("intro")),
),
)
fmt.Println("<!DOCTYPE html>")
fmt.Println(Render(page))
}
loginForm := Form(
Action("/login"),
Method("POST"),
Div(
FormLabel(For("email"), T("Email")),
Input(
InputType("email"),
InputName("email"),
Id("email"),
Required(),
),
),
Div(
FormLabel(For("password"), T("Password")),
Input(
InputType("password"),
InputName("password"),
Id("password"),
Required(),
),
),
Button(
ButtonType("submit"),
T("Login"),
),
)
func homeHandler(w http.ResponseWriter, r *http.Request) {
page := Html(
Lang("en"),
Head(HeadTitle(T("Home"))),
Body(
H1(T("Welcome")),
P(T("This page was built with Plain")),
),
)
w.Header().Set("Content-Type", "text/html")
fmt.Fprint(w, "<!DOCTYPE html>\n")
fmt.Fprint(w, Render(page))
}
- Component: Interface implemented by all HTML elements
- Node: Struct representing an HTML element with tag, attributes, and children
- TextNode: Represents plain text content
- Global: Option type that works with any HTML element (class, id, etc.)
Each HTML element has dedicated option types:
InputType()
,InputName()
,Required()
forInput()
Href()
,Target()
,Rel()
forA()
ButtonType()
,Disabled()
forButton()
- And so on for all HTML5 elements
├── core_node.go # Component interface, Node struct, Render()
├── core_global.go # GlobalAttrs struct, attribute helpers
├── core_options.go # Global option constructors (Class, Id, etc.)
├── options_content.go # Text() and Child() helpers
├── tag_div.go # Div component and options
├── tag_input.go # Input component and options
├── tag_form.go # Form, Input, Textarea, Button, etc.
├── tag_semantic.go # Header, Nav, Main, Section, etc.
└── ... # One file per logical group of elements
The codebase is designed for easy contribution:
- Add a new HTML element: Create
tag_newelem.go
following existing patterns - Add element-specific attributes: Define option types and
apply*
methods - Test: Add examples to
sample/
directory - Document: Update this README with usage examples
Each tag file follows the same pattern:
- Attrs struct with element-specific fields
- Arg interface for type safety
- Constructor function accepting variadic args
- Option types and constructors
- Apply methods connecting options to attributes
- writeAttrs method for HTML generation
MIT