about summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--README.md116
-rw-r--r--attr/boolean.go45
-rw-r--r--attr/boolean_test.go32
-rw-r--r--attr/simple_test.go51
-rw-r--r--components/attributes.go (renamed from attr/attributes.go)7
-rw-r--r--components/attributes_test.go (renamed from attr/attributes_test.go)10
-rw-r--r--components/documents.go29
-rw-r--r--components/documents_test.go13
-rw-r--r--el/elements.go68
-rw-r--r--el/elements_test.go82
-rw-r--r--el/simple.go257
-rw-r--r--el/simple_test.go97
-rw-r--r--el/text.go131
-rw-r--r--el/text_test.go52
-rw-r--r--examples/dot-import/dot-import.go43
-rw-r--r--examples/simple/simple.go30
-rw-r--r--html/attributes.go (renamed from attr/simple.go)48
-rw-r--r--html/attributes_test.go73
-rw-r--r--html/elements.go447
-rw-r--r--html/elements_test.go212
20 files changed, 910 insertions, 933 deletions
diff --git a/README.md b/README.md
index cba1e66..cd8a887 100644
--- a/README.md
+++ b/README.md
@@ -8,19 +8,19 @@ gomponents aims to make it easy to build HTML5 pages of reusable components,
 without the use of a template language. Think server-side-rendered React,
 but without the virtual DOM and diffing.
 
-The implementation is still incomplete, but usable. The API may change until version 1 is reached.
+The implementation is very usable, but the API may change until version 1 is reached.
 
 Check out the blog post [gomponents: declarative view components in Go](https://www.maragu.dk/blog/gomponents-declarative-view-components-in-go/)
 for background.
 
 ## Features
 
+- Build reusable view components
 - Write declarative HTML5 in Go without all the strings, so you get
   - Type safety
   - Auto-completion
   - Nice formatting with `gofmt`
-- Simple API that's easy to learn and use
-- Build reusable view components
+- Simple API that's easy to learn and use (you know most already if you know HTML)
 - No external dependencies
 
 ## Usage
@@ -31,7 +31,8 @@ Get the library using `go get`:
 go get -u github.com/maragudk/gomponents
 ```
 
-Then do something like this:
+The preferred way to use gomponents is with so-called dot-imports (note the dot before the `gomponents/html` import),
+to give you that smooth, native HTML feel:
 
 ```go
 package main
@@ -40,47 +41,51 @@ import (
 	"net/http"
 
 	g "github.com/maragudk/gomponents"
-	"github.com/maragudk/gomponents/attr"
-	"github.com/maragudk/gomponents/el"
+	c "github.com/maragudk/gomponents/components"
+	. "github.com/maragudk/gomponents/html"
 )
 
 func main() {
-	_ = http.ListenAndServe("localhost:8080", handler())
+	_ = http.ListenAndServe("localhost:8080", http.HandlerFunc(handler))
 }
 
-func handler() http.HandlerFunc {
-	return func(w http.ResponseWriter, r *http.Request) {
-		page := Page("Hi!", r.URL.Path)
-		_ = page.Render(w)
-	}
+func handler(w http.ResponseWriter, r *http.Request) {
+	_ = Page("Hi!", r.URL.Path).Render(w)
 }
 
-func Page(title, path string) g.Node {
-	return el.Document(
-		el.HTML(
-			g.Attr("lang", "en"),
-			el.Head(
-				el.Title(title),
-				el.Style(g.Attr("type", "text/css"), g.Raw(".is-active{font-weight: bold}")),
+func Page(title, currentPath string) g.Node {
+	return Document(
+		HTML(
+			Lang("en"),
+			Head(
+				TitleEl(title),
+				StyleEl(Type("text/css"), g.Raw(".is-active{ font-weight: bold }")),
 			),
-			el.Body(
-				Navbar(path),
-				el.H1(title),
-				el.P(g.Textf("Welcome to the page at %v.", path)),
+			Body(
+				Navbar(currentPath),
+				H1(title),
+				P(g.Textf("Welcome to the page at %v.", currentPath)),
 			),
 		),
 	)
 }
 
-func Navbar(path string) g.Node {
-	return g.El("nav",
-		el.A("/", attr.Classes{"is-active": path == "/"}, g.Text("Home")),
-		el.A("/about", attr.Classes{"is-active": path == "/about"}, g.Text("About")),
+func Navbar(currentPath string) g.Node {
+	return Nav(
+		NavbarLink("/", "Home", currentPath),
+		NavbarLink("/about", "About", currentPath),
 	)
 }
+
+func NavbarLink(href, name, currentPath string) g.Node {
+	return A(href, c.Classes{"is-active": currentPath == href}, g.Text(name))
+}
 ```
 
-You could also use a page template to simplify your code a bit:
+Some people don't like dot-imports, and luckily it's completely optional.
+If you don't like dot-imports, just use regular imports.
+
+You could also use the provided HTML5 document template to simplify your code a bit:
 
 ```go
 package main
@@ -89,41 +94,52 @@ import (
 	"net/http"
 
 	g "github.com/maragudk/gomponents"
-	"github.com/maragudk/gomponents/attr"
-	c "github.com/maragudk/gomponents/components"
-	"github.com/maragudk/gomponents/el"
+	. "github.com/maragudk/gomponents/components"
+	. "github.com/maragudk/gomponents/html"
 )
 
 func main() {
-	_ = http.ListenAndServe("localhost:8080", handler())
+	_ = http.ListenAndServe("localhost:8080", http.HandlerFunc(handler))
 }
 
-func handler() http.HandlerFunc {
-	return func(w http.ResponseWriter, r *http.Request) {
-		page := Page("Hi!", r.URL.Path)
-		_ = page.Render(w)
-	}
+func handler(w http.ResponseWriter, r *http.Request) {
+	_ = Page("Hi!", r.URL.Path).Render(w)
 }
 
-func Page(title, path string) g.Node {
-	return c.HTML5(c.DocumentProps{
-		Title:       title,
-		Language:    "en",
-		Head:        []g.Node{el.Style(g.Attr("type", "text/css"), g.Raw(".is-active{font-weight: bold}"))},
-		Body:        []g.Node{
-			Navbar(path),
-			el.H1(title),
-			el.P(g.Textf("Welcome to the page at %v.", path)),
+func Page(title, currentPath string) g.Node {
+	return HTML5(HTML5Props{
+		Title:    title,
+		Language: "en",
+		Head: []g.Node{
+			StyleEl(Type("text/css"), g.Raw(".is-active{ font-weight: bold }")),
+		},
+		Body: []g.Node{
+			Navbar(currentPath),
+			H1(title),
+			P(g.Textf("Welcome to the page at %v.", currentPath)),
 		},
 	})
 }
 
-func Navbar(path string) g.Node {
-	return g.El("nav",
-		el.A("/", attr.Classes{"is-active": path == "/"}, g.Text("Home")),
-		el.A("/about", attr.Classes{"is-active": path == "/about"}, g.Text("About")),
+func Navbar(currentPath string) g.Node {
+	return Nav(
+		NavbarLink("/", "Home", currentPath),
+		NavbarLink("/about", "About", currentPath),
 	)
 }
+
+func NavbarLink(href, name, currentPath string) g.Node {
+	return A(href, Classes{"is-active": currentPath == href}, g.Text(name))
+}
 ```
 
 For more complete examples, see [the examples directory](examples/).
+
+### What's up with the specially named elements and attributes?
+
+Unfortunately, there are three main name clashes in HTML elements and attributes, so they need an `El` or `Attr` suffix,
+respectively, to be able to co-exist in the same package in Go:
+
+- `form` (`FormEl`/`FormAttr`)
+- `style` (`StyleEl`/`StyleAttr`)
+- `title` (`TitleEl`/`TitleAttr`)
diff --git a/attr/boolean.go b/attr/boolean.go
deleted file mode 100644
index 2a70c9d..0000000
--- a/attr/boolean.go
+++ /dev/null
@@ -1,45 +0,0 @@
-package attr
-
-import (
-	g "github.com/maragudk/gomponents"
-)
-
-func Async() g.Node {
-	return g.Attr("async")
-}
-
-func AutoFocus() g.Node {
-	return g.Attr("autofocus")
-}
-
-func AutoPlay() g.Node {
-	return g.Attr("autoplay")
-}
-
-func Controls() g.Node {
-	return g.Attr("controls")
-}
-
-func Defer() g.Node {
-	return g.Attr("defer")
-}
-
-func Disabled() g.Node {
-	return g.Attr("disabled")
-}
-
-func Multiple() g.Node {
-	return g.Attr("multiple")
-}
-
-func ReadOnly() g.Node {
-	return g.Attr("readonly")
-}
-
-func Required() g.Node {
-	return g.Attr("required")
-}
-
-func Selected() g.Node {
-	return g.Attr("selected")
-}
diff --git a/attr/boolean_test.go b/attr/boolean_test.go
deleted file mode 100644
index 309b025..0000000
--- a/attr/boolean_test.go
+++ /dev/null
@@ -1,32 +0,0 @@
-package attr_test
-
-import (
-	"fmt"
-	"testing"
-
-	g "github.com/maragudk/gomponents"
-	"github.com/maragudk/gomponents/assert"
-	"github.com/maragudk/gomponents/attr"
-)
-
-func TestBooleanAttributes(t *testing.T) {
-	cases := map[string]func() g.Node{
-		"async":     attr.Async,
-		"autofocus": attr.AutoFocus,
-		"autoplay":  attr.AutoPlay,
-		"controls":  attr.Controls,
-		"defer":     attr.Defer,
-		"disabled":  attr.Disabled,
-		"multiple":  attr.Multiple,
-		"readonly":  attr.ReadOnly,
-		"required":  attr.Required,
-		"selected":  attr.Selected,
-	}
-
-	for name, fn := range cases {
-		t.Run(fmt.Sprintf("should output %v", name), func(t *testing.T) {
-			n := g.El("div", fn())
-			assert.Equal(t, fmt.Sprintf(`<div %v></div>`, name), n)
-		})
-	}
-}
diff --git a/attr/simple_test.go b/attr/simple_test.go
deleted file mode 100644
index d006b76..0000000
--- a/attr/simple_test.go
+++ /dev/null
@@ -1,51 +0,0 @@
-package attr_test
-
-import (
-	"fmt"
-	"testing"
-
-	g "github.com/maragudk/gomponents"
-	"github.com/maragudk/gomponents/assert"
-	"github.com/maragudk/gomponents/attr"
-)
-
-func TestSimpleAttributes(t *testing.T) {
-	cases := map[string]func(string) g.Node{
-		"accept":       attr.Accept,
-		"autocomplete": attr.AutoComplete,
-		"charset":      attr.Charset,
-		"class":        attr.Class,
-		"cols":         attr.Cols,
-		"content":      attr.Content,
-		"form":         attr.Form,
-		"height":       attr.Height,
-		"href":         attr.Href,
-		"id":           attr.ID,
-		"lang":         attr.Lang,
-		"max":          attr.Max,
-		"maxlength":    attr.MaxLength,
-		"min":          attr.Min,
-		"minlength":    attr.MinLength,
-		"name":         attr.Name,
-		"pattern":      attr.Pattern,
-		"preload":      attr.Preload,
-		"placeholder":  attr.Placeholder,
-		"rel":          attr.Rel,
-		"rows":         attr.Rows,
-		"src":          attr.Src,
-		"style":        attr.Style,
-		"tabindex":     attr.TabIndex,
-		"target":       attr.Target,
-		"title":        attr.Title,
-		"type":         attr.Type,
-		"value":        attr.Value,
-		"width":        attr.Width,
-	}
-
-	for name, fn := range cases {
-		t.Run(fmt.Sprintf(`should output %v="hat"`, name), func(t *testing.T) {
-			n := g.El("div", fn("hat"))
-			assert.Equal(t, fmt.Sprintf(`<div %v="hat"></div>`, name), n)
-		})
-	}
-}
diff --git a/attr/attributes.go b/components/attributes.go
index 938d762..dc7ef9b 100644
--- a/attr/attributes.go
+++ b/components/attributes.go
@@ -1,6 +1,4 @@
-// Package attr provides shortcuts and helpers to common HTML attributes.
-// See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes for a list of attributes.
-package attr
+package components
 
 import (
 	"io"
@@ -8,6 +6,7 @@ import (
 	"strings"
 
 	g "github.com/maragudk/gomponents"
+	"github.com/maragudk/gomponents/html"
 )
 
 // Classes is a map of strings to booleans, which Renders to an attribute with name "class".
@@ -23,7 +22,7 @@ func (c Classes) Render(w io.Writer) error {
 		}
 	}
 	sort.Strings(included)
-	return g.Attr("class", strings.Join(included, " ")).Render(w)
+	return html.Class(strings.Join(included, " ")).Render(w)
 }
 
 func (c Classes) Type() g.NodeType {
diff --git a/attr/attributes_test.go b/components/attributes_test.go
index da04a20..bf3e93b 100644
--- a/attr/attributes_test.go
+++ b/components/attributes_test.go
@@ -1,16 +1,16 @@
-package attr_test
+package components_test
 
 import (
 	"testing"
 
 	g "github.com/maragudk/gomponents"
 	"github.com/maragudk/gomponents/assert"
-	"github.com/maragudk/gomponents/attr"
+	c "github.com/maragudk/gomponents/components"
 )
 
 func TestClasses(t *testing.T) {
 	t.Run("given a map, returns sorted keys from the map with value true", func(t *testing.T) {
-		assert.Equal(t, ` class="boheme-hat hat partyhat"`, attr.Classes{
+		assert.Equal(t, ` class="boheme-hat hat partyhat"`, c.Classes{
 			"boheme-hat": true,
 			"hat":        true,
 			"partyhat":   true,
@@ -19,12 +19,12 @@ func TestClasses(t *testing.T) {
 	})
 
 	t.Run("renders as attribute in an element", func(t *testing.T) {
-		e := g.El("div", attr.Classes{"hat": true})
+		e := g.El("div", c.Classes{"hat": true})
 		assert.Equal(t, `<div class="hat"></div>`, e)
 	})
 
 	t.Run("also works with fmt", func(t *testing.T) {
-		a := attr.Classes{"hat": true}
+		a := c.Classes{"hat": true}
 		if a.String() != ` class="hat"` {
 			t.FailNow()
 		}
diff --git a/components/documents.go b/components/documents.go
index 5e96466..e2d3a22 100644
--- a/components/documents.go
+++ b/components/documents.go
@@ -1,15 +1,14 @@
-// Package components provides high-level components that are composed of low-level elements and attributes.
+// Package components provides high-level components and helpers that are composed of low-level elements and attributes.
 package components
 
 import (
 	g "github.com/maragudk/gomponents"
-	"github.com/maragudk/gomponents/attr"
-	"github.com/maragudk/gomponents/el"
+	. "github.com/maragudk/gomponents/html"
 )
 
-// DocumentProps for HTML5.
+// HTML5Props for HTML5.
 // Title is set no matter what, Description and Language elements only if the strings are non-empty.
-type DocumentProps struct {
+type HTML5Props struct {
 	Title       string
 	Description string
 	Language    string
@@ -18,24 +17,24 @@ type DocumentProps struct {
 }
 
 // HTML5 document template.
-func HTML5(p DocumentProps) g.NodeFunc {
+func HTML5(p HTML5Props) g.NodeFunc {
 	var lang, description g.Node
 	if p.Language != "" {
-		lang = attr.Lang(p.Language)
+		lang = Lang(p.Language)
 	}
 	if p.Description != "" {
-		description = el.Meta(attr.Name("description"), attr.Content(p.Description))
+		description = Meta(Name("description"), Content(p.Description))
 	}
-	return el.Document(
-		el.HTML(lang,
-			el.Head(
-				el.Meta(attr.Charset("utf-8")),
-				el.Meta(attr.Name("viewport"), attr.Content("width=device-width, initial-scale=1")),
-				el.Title(p.Title),
+	return Document(
+		HTML(lang,
+			Head(
+				Meta(Charset("utf-8")),
+				Meta(Name("viewport"), Content("width=device-width, initial-scale=1")),
+				TitleEl(p.Title),
 				description,
 				g.Group(p.Head),
 			),
-			el.Body(g.Group(p.Body)),
+			Body(g.Group(p.Body)),
 		),
 	)
 }
diff --git a/components/documents_test.go b/components/documents_test.go
index 863ac96..07f8697 100644
--- a/components/documents_test.go
+++ b/components/documents_test.go
@@ -5,26 +5,25 @@ import (
 
 	g "github.com/maragudk/gomponents"
 	"github.com/maragudk/gomponents/assert"
-	"github.com/maragudk/gomponents/attr"
-	c "github.com/maragudk/gomponents/components"
-	"github.com/maragudk/gomponents/el"
+	. "github.com/maragudk/gomponents/components"
+	. "github.com/maragudk/gomponents/html"
 )
 
 func TestHTML5(t *testing.T) {
 	t.Run("returns an html5 document template", func(t *testing.T) {
-		e := c.HTML5(c.DocumentProps{
+		e := HTML5(HTML5Props{
 			Title:       "Hat",
 			Description: "Love hats.",
 			Language:    "en",
-			Head:        []g.Node{el.Link(attr.Rel("stylesheet"), attr.Href("/hat.css"))},
-			Body:        []g.Node{el.Div()},
+			Head:        []g.Node{Link(Rel("stylesheet"), Href("/hat.css"))},
+			Body:        []g.Node{Div()},
 		})
 
 		assert.Equal(t, `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Hat</title><meta name="description" content="Love hats."><link rel="stylesheet" href="/hat.css"></head><body><div></div></body></html>`, e)
 	})
 
 	t.Run("returns no language, description, and extra head/body elements if empty", func(t *testing.T) {
-		e := c.HTML5(c.DocumentProps{
+		e := HTML5(HTML5Props{
 			Title: "Hat",
 		})
 
diff --git a/el/elements.go b/el/elements.go
deleted file mode 100644
index 0841ff8..0000000
--- a/el/elements.go
+++ /dev/null
@@ -1,68 +0,0 @@
-// Package el provides shortcuts and helpers to common HTML elements.
-// See https://developer.mozilla.org/en-US/docs/Web/HTML/Element for a list of elements.
-package el
-
-import (
-	"fmt"
-	"io"
-
-	g "github.com/maragudk/gomponents"
-)
-
-func A(href string, children ...g.Node) g.NodeFunc {
-	return g.El("a", g.Attr("href", href), g.Group(children))
-}
-
-// Document returns an special kind of Node that prefixes its child with the string "<!doctype html>".
-func Document(child g.Node) g.NodeFunc {
-	return func(w io.Writer) error {
-		if _, err := w.Write([]byte("<!doctype html>")); err != nil {
-			return err
-		}
-		return child.Render(w)
-	}
-}
-
-// Form returns an element with name "form", the given action and method attributes, and the given children.
-func Form(action, method string, children ...g.Node) g.NodeFunc {
-	return g.El("form", g.Attr("action", action), g.Attr("method", method), g.Group(children))
-}
-
-func Img(src, alt string, children ...g.Node) g.NodeFunc {
-	return g.El("img", g.Attr("src", src), g.Attr("alt", alt), g.Group(children))
-}
-
-// Input returns an element with name "input", the given type and name attributes, and the given children.
-// Note that "type" is a keyword in Go, so the parameter is called typ.
-func Input(typ, name string, children ...g.Node) g.NodeFunc {
-	return g.El("input", g.Attr("type", typ), g.Attr("name", name), g.Group(children))
-}
-
-// Label returns an element with name "label", the given for attribute, and the given children.
-// Note that "for" is a keyword in Go, so the parameter is called forr.
-func Label(forr string, children ...g.Node) g.NodeFunc {
-	return g.El("label", g.Attr("for", forr), g.Group(children))
-}
-
-// Option returns an element with name "option", the given text content and value attribute, and the given children.
-func Option(text, value string, children ...g.Node) g.NodeFunc {
-	return g.El("option", g.Attr("value", value), g.Text(text), g.Group(children))
-}
-
-// Progress returns an element with name "progress", the given value and max attributes, and the given children.
-func Progress(value, max float64, children ...g.Node) g.NodeFunc {
-	return g.El("progress",
-		g.Attr("value", fmt.Sprintf("%v", value)),
-		g.Attr("max", fmt.Sprintf("%v", max)),
-		g.Group(children))
-}
-
-// Select returns an element with name "select", the given name attribute, and the given children.
-func Select(name string, children ...g.Node) g.NodeFunc {
-	return g.El("select", g.Attr("name", name), g.Group(children))
-}
-
-// Textarea returns an element with name "textarea", the given name attribute, and the given children.
-func Textarea(name string, children ...g.Node) g.NodeFunc {
-	return g.El("textarea", g.Attr("name", name), g.Group(children))
-}
diff --git a/el/elements_test.go b/el/elements_test.go
deleted file mode 100644
index adce6df..0000000
--- a/el/elements_test.go
+++ /dev/null
@@ -1,82 +0,0 @@
-package el_test
-
-import (
-	"errors"
-	"testing"
-
-	g "github.com/maragudk/gomponents"
-	"github.com/maragudk/gomponents/assert"
-	"github.com/maragudk/gomponents/el"
-)
-
-type erroringWriter struct{}
-
-func (w *erroringWriter) Write(p []byte) (n int, err error) {
-	return 0, errors.New("don't want to write")
-}
-
-func TestDocument(t *testing.T) {
-	t.Run("returns doctype and children", func(t *testing.T) {
-		assert.Equal(t, `<!doctype html><html></html>`, el.Document(g.El("html")))
-	})
-
-	t.Run("errors on write error in Render", func(t *testing.T) {
-		err := el.Document(g.El("html")).Render(&erroringWriter{})
-		assert.Error(t, err)
-	})
-}
-
-func TestForm(t *testing.T) {
-	t.Run("returns a form element with action and method attributes", func(t *testing.T) {
-		assert.Equal(t, `<form action="/" method="post"></form>`, el.Form("/", "post"))
-	})
-}
-
-func TestInput(t *testing.T) {
-	t.Run("returns an input element with attributes type and name", func(t *testing.T) {
-		assert.Equal(t, `<input type="text" name="hat">`, el.Input("text", "hat"))
-	})
-}
-
-func TestLabel(t *testing.T) {
-	t.Run("returns a label element with attribute for", func(t *testing.T) {
-		assert.Equal(t, `<label for="hat">Hat</label>`, el.Label("hat", g.Text("Hat")))
-	})
-}
-
-func TestOption(t *testing.T) {
-	t.Run("returns an option element with attribute label and content", func(t *testing.T) {
-		assert.Equal(t, `<option value="hat">Hat</option>`, el.Option("Hat", "hat"))
-	})
-}
-
-func TestProgress(t *testing.T) {
-	t.Run("returns a progress element with attributes value and max", func(t *testing.T) {
-		assert.Equal(t, `<progress value="5.5" max="10"></progress>`, el.Progress(5.5, 10))
-	})
-}
-
-func TestSelect(t *testing.T) {
-	t.Run("returns a select element with attribute name", func(t *testing.T) {
-		assert.Equal(t, `<select name="hat"><option value="partyhat">Partyhat</option></select>`,
-			el.Select("hat", el.Option("Partyhat", "partyhat")))
-	})
-}
-
-func TestTextarea(t *testing.T) {
-	t.Run("returns a textarea element with attribute name", func(t *testing.T) {
-		assert.Equal(t, `<textarea name="hat"></textarea>`, el.Textarea("hat"))
-	})
-}
-
-func TestA(t *testing.T) {
-	t.Run("returns an a element with a href attribute", func(t *testing.T) {
-		assert.Equal(t, `<a href="#">hat</a>`, el.A("#", g.Text("hat")))
-	})
-}
-
-func TestImg(t *testing.T) {
-	t.Run("returns an img element with href and alt attributes", func(t *testing.T) {
-		assert.Equal(t, `<img src="hat.png" alt="hat" id="image">`, el.Img("hat.png", "hat", g.Attr("id", "image")))
-	})
-}
diff --git a/el/simple.go b/el/simple.go
deleted file mode 100644
index 1ebf33f..0000000
--- a/el/simple.go
+++ /dev/null
@@ -1,257 +0,0 @@
-package el
-
-import (
-	g "github.com/maragudk/gomponents"
-)
-
-func Address(children ...g.Node) g.NodeFunc {
-	return g.El("address", children...)
-}
-
-func Area(children ...g.Node) g.NodeFunc {
-	return g.El("area", children...)
-}
-
-func Article(children ...g.Node) g.NodeFunc {
-	return g.El("article", children...)
-}
-
-func Aside(children ...g.Node) g.NodeFunc {
-	return g.El("aside", children...)
-}
-
-func Audio(children ...g.Node) g.NodeFunc {
-	return g.El("audio", children...)
-}
-
-func Base(children ...g.Node) g.NodeFunc {
-	return g.El("base", children...)
-}
-
-func BlockQuote(children ...g.Node) g.NodeFunc {
-	return g.El("blockquote", children...)
-}
-
-func Body(children ...g.Node) g.NodeFunc {
-	return g.El("body", children...)
-}
-
-func Br(children ...g.Node) g.NodeFunc {
-	return g.El("br", children...)
-}
-
-func Button(children ...g.Node) g.NodeFunc {
-	return g.El("button", children...)
-}
-
-func Canvas(children ...g.Node) g.NodeFunc {
-	return g.El("canvas", children...)
-}
-
-func Cite(children ...g.Node) g.NodeFunc {
-	return g.El("cite", children...)
-}
-
-func Code(children ...g.Node) g.NodeFunc {
-	return g.El("code", children...)
-}
-
-func Col(children ...g.Node) g.NodeFunc {
-	return g.El("col", children...)
-}
-
-func ColGroup(children ...g.Node) g.NodeFunc {
-	return g.El("colgroup", children...)
-}
-
-func Data(children ...g.Node) g.NodeFunc {
-	return g.El("data", children...)
-}
-
-func DataList(children ...g.Node) g.NodeFunc {
-	return g.El("datalist", children...)
-}
-
-func Details(children ...g.Node) g.NodeFunc {
-	return g.El("details", children...)
-}
-
-func Dialog(children ...g.Node) g.NodeFunc {
-	return g.El("dialog", children...)
-}
-
-func Div(children ...g.Node) g.NodeFunc {
-	return g.El("div", children...)
-}
-
-func Dl(children ...g.Node) g.NodeFunc {
-	return g.El("dl", children...)
-}
-
-func Embed(children ...g.Node) g.NodeFunc {
-	return g.El("embed", children...)
-}
-
-func FieldSet(children ...g.Node) g.NodeFunc {
-	return g.El("fieldset", children...)
-}
-
-func Figure(children ...g.Node) g.NodeFunc {
-	return g.El("figure", children...)
-}
-
-func Footer(children ...g.Node) g.NodeFunc {
-	return g.El("footer", children...)
-}
-
-func Head(children ...g.Node) g.NodeFunc {
-	return g.El("head", children...)
-}
-
-func Header(children ...g.Node) g.NodeFunc {
-	return g.El("header", children...)
-}
-
-func HGroup(children ...g.Node) g.NodeFunc {
-	return g.El("hgroup", children...)
-}
-
-func Hr(children ...g.Node) g.NodeFunc {
-	return g.El("hr", children...)
-}
-
-func HTML(children ...g.Node) g.NodeFunc {
-	return g.El("html", children...)
-}
-
-func IFrame(children ...g.Node) g.NodeFunc {
-	return g.El("iframe", children...)
-}
-
-func Legend(children ...g.Node) g.NodeFunc {
-	return g.El("legend", children...)
-}
-
-func Li(children ...g.Node) g.NodeFunc {
-	return g.El("li", children...)
-}
-
-func Link(children ...g.Node) g.NodeFunc {
-	return g.El("link", children...)
-}
-
-func Main(children ...g.Node) g.NodeFunc {
-	return g.El("main", children...)
-}
-
-func Menu(children ...g.Node) g.NodeFunc {
-	return g.El("menu", children...)
-}
-
-func Meta(children ...g.Node) g.NodeFunc {
-	return g.El("meta", children...)
-}
-
-func Meter(children ...g.Node) g.NodeFunc {
-	return g.El("meter", children...)
-}
-
-func Nav(children ...g.Node) g.NodeFunc {
-	return g.El("nav", children...)
-}
-
-func NoScript(children ...g.Node) g.NodeFunc {
-	return g.El("noscript", children...)
-}
-
-func Object(children ...g.Node) g.NodeFunc {
-	return g.El("object", children...)
-}
-
-func Ol(children ...g.Node) g.NodeFunc {
-	return g.El("ol", children...)
-}
-
-func OptGroup(children ...g.Node) g.NodeFunc {
-	return g.El("optgroup", children...)
-}
-
-func P(children ...g.Node) g.NodeFunc {
-	return g.El("p", children...)
-}
-
-func Param(children ...g.Node) g.NodeFunc {
-	return g.El("param", children...)
-}
-
-func Picture(children ...g.Node) g.NodeFunc {
-	return g.El("picture", children...)
-}
-
-func Pre(children ...g.Node) g.NodeFunc {
-	return g.El("pre", children...)
-}
-
-func Script(children ...g.Node) g.NodeFunc {
-	return g.El("script", children...)
-}
-
-func Section(children ...g.Node) g.NodeFunc {
-	return g.El("section", children...)
-}
-
-func Source(children ...g.Node) g.NodeFunc {
-	return g.El("source", children...)
-}
-
-func Span(children ...g.Node) g.NodeFunc {
-	return g.El("span", children...)
-}
-
-func Style(children ...g.Node) g.NodeFunc {
-	return g.El("style", children...)
-}
-
-func Summary(children ...g.Node) g.NodeFunc {
-	return g.El("summary", children...)
-}
-
-func SVG(children ...g.Node) g.NodeFunc {
-	return g.El("svg", children...)
-}
-
-func Table(children ...g.Node) g.NodeFunc {
-	return g.El("table", children...)
-}
-
-func TBody(children ...g.Node) g.NodeFunc {
-	return g.El("tbody", children...)
-}
-
-func Td(children ...g.Node) g.NodeFunc {
-	return g.El("td", children...)
-}
-
-func TFoot(children ...g.Node) g.NodeFunc {
-	return g.El("tfoot", children...)
-}
-
-func Th(children ...g.Node) g.NodeFunc {
-	return g.El("th", children...)
-}
-
-func THead(children ...g.Node) g.NodeFunc {
-	return g.El("thead", children...)
-}
-
-func Tr(children ...g.Node) g.NodeFunc {
-	return g.El("tr", children...)
-}
-
-func Ul(children ...g.Node) g.NodeFunc {
-	return g.El("ul", children...)
-}
-
-func Wbr(children ...g.Node) g.NodeFunc {
-	return g.El("wbr", children...)
-}
diff --git a/el/simple_test.go b/el/simple_test.go
deleted file mode 100644
index 2621982..0000000
--- a/el/simple_test.go
+++ /dev/null
@@ -1,97 +0,0 @@
-package el_test
-
-import (
-	"fmt"
-	"testing"
-
-	g "github.com/maragudk/gomponents"
-	"github.com/maragudk/gomponents/assert"
-	"github.com/maragudk/gomponents/el"
-)
-
-func TestSimpleElements(t *testing.T) {
-	cases := map[string]func(...g.Node) g.NodeFunc{
-		"address":    el.Address,
-		"article":    el.Article,
-		"aside":      el.Aside,
-		"audio":      el.Audio,
-		"blockquote": el.BlockQuote,
-		"body":       el.Body,
-		"button":     el.Button,
-		"canvas":     el.Canvas,
-		"cite":       el.Cite,
-		"code":       el.Code,
-		"colgroup":   el.ColGroup,
-		"data":       el.Data,
-		"datalist":   el.DataList,
-		"details":    el.Details,
-		"dialog":     el.Dialog,
-		"div":        el.Div,
-		"dl":         el.Dl,
-		"fieldset":   el.FieldSet,
-		"figure":     el.Figure,
-		"footer":     el.Footer,
-		"head":       el.Head,
-		"header":     el.Header,
-		"hgroup":     el.HGroup,
-		"html":       el.HTML,
-		"iframe":     el.IFrame,
-		"legend":     el.Legend,
-		"li":         el.Li,
-		"main":       el.Main,
-		"menu":       el.Menu,
-		"meter":      el.Meter,
-		"nav":        el.Nav,
-		"noscript":   el.NoScript,
-		"object":     el.Object,
-		"ol":         el.Ol,
-		"optgroup":   el.OptGroup,
-		"p":          el.P,
-		"picture":    el.Picture,
-		"pre":        el.Pre,
-		"script":     el.Script,
-		"section":    el.Section,
-		"span":       el.Span,
-		"style":      el.Style,
-		"summary":    el.Summary,
-		"svg":        el.SVG,
-		"table":      el.Table,
-		"tbody":      el.TBody,
-		"td":         el.Td,
-		"tfoot":      el.TFoot,
-		"th":         el.Th,
-		"thead":      el.THead,
-		"tr":         el.Tr,
-		"ul":         el.Ul,
-	}
-
-	for name, fn := range cases {
-		t.Run(fmt.Sprintf("should output %v", name), func(t *testing.T) {
-			n := fn(g.Attr("id", "hat"))
-			assert.Equal(t, fmt.Sprintf(`<%v id="hat"></%v>`, name, name), n)
-		})
-	}
-}
-
-func TestSimpleVoidKindElements(t *testing.T) {
-	cases := map[string]func(...g.Node) g.NodeFunc{
-		"area":   el.Area,
-		"base":   el.Base,
-		"br":     el.Br,
-		"col":    el.Col,
-		"embed":  el.Embed,
-		"hr":     el.Hr,
-		"link":   el.Link,
-		"meta":   el.Meta,
-		"param":  el.Param,
-		"source": el.Source,
-		"wbr":    el.Wbr,
-	}
-
-	for name, fn := range cases {
-		t.Run(fmt.Sprintf("should output %v", name), func(t *testing.T) {
-			n := fn(g.Attr("id", "hat"))
-			assert.Equal(t, fmt.Sprintf(`<%v id="hat">`, name), n)
-		})
-	}
-}
diff --git a/el/text.go b/el/text.go
deleted file mode 100644
index 6ab88db..0000000
--- a/el/text.go
+++ /dev/null
@@ -1,131 +0,0 @@
-package el
-
-import (
-	g "github.com/maragudk/gomponents"
-)
-
-func Abbr(text string, children ...g.Node) g.NodeFunc {
-	return g.El("abbr", g.Text(text), g.Group(children))
-}
-
-func B(text string, children ...g.Node) g.NodeFunc {
-	return g.El("b", g.Text(text), g.Group(children))
-}
-
-func Caption(text string, children ...g.Node) g.NodeFunc {
-	return g.El("caption", g.Text(text), g.Group(children))
-}
-
-func Dd(text string, children ...g.Node) g.NodeFunc {
-	return g.El("dd", g.Text(text), g.Group(children))
-}
-
-func Del(text string, children ...g.Node) g.NodeFunc {
-	return g.El("del", g.Text(text), g.Group(children))
-}
-
-func Dfn(text string, children ...g.Node) g.NodeFunc {
-	return g.El("dfn", g.Text(text), g.Group(children))
-}
-
-func Dt(text string, children ...g.Node) g.NodeFunc {
-	return g.El("dt", g.Text(text), g.Group(children))
-}
-
-func Em(text string, children ...g.Node) g.NodeFunc {
-	return g.El("em", g.Text(text), g.Group(children))
-}
-
-func FigCaption(text string, children ...g.Node) g.NodeFunc {
-	return g.El("figcaption", g.Text(text), g.Group(children))
-}
-
-// H1 returns an element with name "h1", the given text content, and the given children.
-func H1(text string, children ...g.Node) g.NodeFunc {
-	return g.El("h1", g.Text(text), g.Group(children))
-}
-
-// H2 returns an element with name "h2", the given text content, and the given children.
-func H2(text string, children ...g.Node) g.NodeFunc {
-	return g.El("h2", g.Text(text), g.Group(children))
-}
-
-// H3 returns an element with name "h3", the given text content, and the given children.
-func H3(text string, children ...g.Node) g.NodeFunc {
-	return g.El("h3", g.Text(text), g.Group(children))
-}
-
-// H4 returns an element with name "h4", the given text content, and the given children.
-func H4(text string, children ...g.Node) g.NodeFunc {
-	return g.El("h4", g.Text(text), g.Group(children))
-}
-
-// H5 returns an element with name "h5", the given text content, and the given children.
-func H5(text string, children ...g.Node) g.NodeFunc {
-	return g.El("h5", g.Text(text), g.Group(children))
-}
-
-// H6 returns an element with name "h6", the given text content, and the given children.
-func H6(text string, children ...g.Node) g.NodeFunc {
-	return g.El("h6", g.Text(text), g.Group(children))
-}
-
-func I(text string, children ...g.Node) g.NodeFunc {
-	return g.El("i", g.Text(text), g.Group(children))
-}
-
-func Ins(text string, children ...g.Node) g.NodeFunc {
-	return g.El("ins", g.Text(text), g.Group(children))
-}
-
-func Kbd(text string, children ...g.Node) g.NodeFunc {
-	return g.El("kbd", g.Text(text), g.Group(children))
-}
-
-func Mark(text string, children ...g.Node) g.NodeFunc {
-	return g.El("mark", g.Text(text), g.Group(children))
-}
-
-func Q(text string, children ...g.Node) g.NodeFunc {
-	return g.El("q", g.Text(text), g.Group(children))
-}
-
-func S(text string, children ...g.Node) g.NodeFunc {
-	return g.El("s", g.Text(text), g.Group(children))
-}
-
-func Samp(text string, children ...g.Node) g.NodeFunc {
-	return g.El("samp", g.Text(text), g.Group(children))
-}
-
-func Small(text string, children ...g.Node) g.NodeFunc {
-	return g.El("small", g.Text(text), g.Group(children))
-}
-
-func Strong(text string, children ...g.Node) g.NodeFunc {
-	return g.El("strong", g.Text(text), g.Group(children))
-}
-
-func Sub(text string, children ...g.Node) g.NodeFunc {
-	return g.El("sub", g.Text(text), g.Group(children))
-}
-
-func Sup(text string, children ...g.Node) g.NodeFunc {
-	return g.El("sup", g.Text(text), g.Group(children))
-}
-
-func Time(text string, children ...g.Node) g.NodeFunc {
-	return g.El("time", g.Text(text), g.Group(children))
-}
-
-func Title(title string, children ...g.Node) g.NodeFunc {
-	return g.El("title", g.Text(title), g.Group(children))
-}
-
-func U(text string, children ...g.Node) g.NodeFunc {
-	return g.El("u", g.Text(text), g.Group(children))
-}
-
-func Var(text string, children ...g.Node) g.NodeFunc {
-	return g.El("var", g.Text(text), g.Group(children))
-}
diff --git a/el/text_test.go b/el/text_test.go
deleted file mode 100644
index a751e19..0000000
--- a/el/text_test.go
+++ /dev/null
@@ -1,52 +0,0 @@
-package el_test
-
-import (
-	"fmt"
-	"testing"
-
-	g "github.com/maragudk/gomponents"
-	"github.com/maragudk/gomponents/assert"
-	"github.com/maragudk/gomponents/el"
-)
-
-func TestTextElements(t *testing.T) {
-	cases := map[string]func(string, ...g.Node) g.NodeFunc{
-		"abbr":       el.Abbr,
-		"b":          el.B,
-		"caption":    el.Caption,
-		"dd":         el.Dd,
-		"del":        el.Del,
-		"dfn":        el.Dfn,
-		"dt":         el.Dt,
-		"em":         el.Em,
-		"figcaption": el.FigCaption,
-		"h1":         el.H1,
-		"h2":         el.H2,
-		"h3":         el.H3,
-		"h4":         el.H4,
-		"h5":         el.H5,
-		"h6":         el.H6,
-		"i":          el.I,
-		"ins":        el.Ins,
-		"kbd":        el.Kbd,
-		"mark":       el.Mark,
-		"q":          el.Q,
-		"s":          el.S,
-		"samp":       el.Samp,
-		"small":      el.Small,
-		"strong":     el.Strong,
-		"sub":        el.Sub,
-		"sup":        el.Sup,
-		"time":       el.Time,
-		"title":      el.Title,
-		"u":          el.U,
-		"var":        el.Var,
-	}
-
-	for name, fn := range cases {
-		t.Run(fmt.Sprintf("should output %v", name), func(t *testing.T) {
-			n := fn("hat", g.Attr("id", "hat"))
-			assert.Equal(t, fmt.Sprintf(`<%v id="hat">hat</%v>`, name, name), n)
-		})
-	}
-}
diff --git a/examples/dot-import/dot-import.go b/examples/dot-import/dot-import.go
index be3bd50..1d595b8 100644
--- a/examples/dot-import/dot-import.go
+++ b/examples/dot-import/dot-import.go
@@ -3,9 +3,9 @@ package main
 import (
 	"net/http"
 
-	. "github.com/maragudk/gomponents"
+	g "github.com/maragudk/gomponents"
 	. "github.com/maragudk/gomponents/components"
-	. "github.com/maragudk/gomponents/el"
+	. "github.com/maragudk/gomponents/html"
 )
 
 func main() {
@@ -13,25 +13,32 @@ func main() {
 }
 
 func handler(w http.ResponseWriter, r *http.Request) {
-	p := page(props{
-		title: r.URL.Path,
-		path:  r.URL.Path,
-	})
-	_ = p.Render(w)
-}
-
-type props struct {
-	title string
-	path  string
+	page := Page("Hi!", r.URL.Path)
+	_ = page.Render(w)
 }
 
-func page(p props) Node {
-	return HTML5(DocumentProps{
-		Title:    p.title,
+func Page(title, currentPath string) g.Node {
+	return HTML5(HTML5Props{
+		Title:    title,
 		Language: "en",
-		Body: []Node{
-			H1(p.title),
-			P(Textf("Welcome to the page at %v.", p.path)),
+		Head: []g.Node{
+			StyleEl(Type("text/css"), g.Raw(".is-active{ font-weight: bold }")),
+		},
+		Body: []g.Node{
+			Navbar(currentPath),
+			H1(title),
+			P(g.Textf("Welcome to the page at %v.", currentPath)),
 		},
 	})
 }
+
+func Navbar(currentPath string) g.Node {
+	return Nav(
+		NavbarLink("/", "Home", currentPath),
+		NavbarLink("/about", "About", currentPath),
+	)
+}
+
+func NavbarLink(href, name, currentPath string) g.Node {
+	return A(href, Classes{"is-active": currentPath == href}, g.Text(name))
+}
diff --git a/examples/simple/simple.go b/examples/simple/simple.go
index dd6dd58..535fa6e 100644
--- a/examples/simple/simple.go
+++ b/examples/simple/simple.go
@@ -5,8 +5,8 @@ import (
 	"time"
 
 	g "github.com/maragudk/gomponents"
-	"github.com/maragudk/gomponents/attr"
-	"github.com/maragudk/gomponents/el"
+	c "github.com/maragudk/gomponents/components"
+	h "github.com/maragudk/gomponents/html"
 )
 
 func main() {
@@ -27,22 +27,22 @@ type props struct {
 }
 
 func page(p props) g.Node {
-	return el.Document(
-		el.HTML(attr.Lang("en"),
-			el.Head(
-				el.Title(p.title),
-				el.Style(attr.Type("text/css"),
+	return h.Document(
+		h.HTML(h.Lang("en"),
+			h.Head(
+				h.TitleEl(p.title),
+				h.StyleEl(h.Type("text/css"),
 					g.Raw(".is-active{font-weight: bold}"),
 					g.Raw("ul.nav { list-style-type: none; margin: 0; padding: 0; overflow: hidden; }"),
 					g.Raw("ul.nav li { display: block;  padding: 8px; float: left; }"),
 				),
 			),
-			el.Body(
+			h.Body(
 				navbar(navbarProps{path: p.path}),
-				el.Hr(),
-				el.H1(p.title),
-				el.P(g.Textf("Welcome to the page at %v.", p.path)),
-				el.P(g.Textf("Rendered at %v", time.Now())),
+				h.Hr(),
+				h.H1(p.title),
+				h.P(g.Textf("Welcome to the page at %v.", p.path)),
+				h.P(g.Textf("Rendered at %v", time.Now())),
 			),
 		),
 	)
@@ -63,9 +63,9 @@ func navbar(props navbarProps) g.Node {
 	}
 	lis := g.Map(len(items), func(i int) g.Node {
 		item := items[i]
-		return el.Li(
-			el.A(item.path, attr.Classes(map[string]bool{"is-active": props.path == item.path}), g.Text(item.text)),
+		return h.Li(
+			h.A(item.path, c.Classes(map[string]bool{"is-active": props.path == item.path}), g.Text(item.text)),
 		)
 	})
-	return el.Ul(attr.Class("nav"), g.Group(lis))
+	return h.Ul(h.Class("nav"), g.Group(lis))
 }
diff --git a/attr/simple.go b/html/attributes.go
index a96c139..3db8584 100644
--- a/attr/simple.go
+++ b/html/attributes.go
@@ -1,9 +1,49 @@
-package attr
+package html
 
 import (
 	g "github.com/maragudk/gomponents"
 )
 
+func Async() g.Node {
+	return g.Attr("async")
+}
+
+func AutoFocus() g.Node {
+	return g.Attr("autofocus")
+}
+
+func AutoPlay() g.Node {
+	return g.Attr("autoplay")
+}
+
+func Controls() g.Node {
+	return g.Attr("controls")
+}
+
+func Defer() g.Node {
+	return g.Attr("defer")
+}
+
+func Disabled() g.Node {
+	return g.Attr("disabled")
+}
+
+func Multiple() g.Node {
+	return g.Attr("multiple")
+}
+
+func ReadOnly() g.Node {
+	return g.Attr("readonly")
+}
+
+func Required() g.Node {
+	return g.Attr("required")
+}
+
+func Selected() g.Node {
+	return g.Attr("selected")
+}
+
 func Accept(v string) g.Node {
 	return g.Attr("accept", v)
 }
@@ -28,7 +68,7 @@ func Content(v string) g.Node {
 	return g.Attr("content", v)
 }
 
-func Form(v string) g.Node {
+func FormAttr(v string) g.Node {
 	return g.Attr("form", v)
 }
 
@@ -92,7 +132,7 @@ func Src(v string) g.Node {
 	return g.Attr("src", v)
 }
 
-func Style(v string) g.Node {
+func StyleAttr(v string) g.Node {
 	return g.Attr("style", v)
 }
 
@@ -104,7 +144,7 @@ func Target(v string) g.Node {
 	return g.Attr("target", v)
 }
 
-func Title(v string) g.Node {
+func TitleAttr(v string) g.Node {
 	return g.Attr("title", v)
 }
 
diff --git a/html/attributes_test.go b/html/attributes_test.go
new file mode 100644
index 0000000..7bf45bb
--- /dev/null
+++ b/html/attributes_test.go
@@ -0,0 +1,73 @@
+package html_test
+
+import (
+	"fmt"
+	"testing"
+
+	g "github.com/maragudk/gomponents"
+	"github.com/maragudk/gomponents/assert"
+	. "github.com/maragudk/gomponents/html"
+)
+
+func TestBooleanAttributes(t *testing.T) {
+	cases := map[string]func() g.Node{
+		"async":     Async,
+		"autofocus": AutoFocus,
+		"autoplay":  AutoPlay,
+		"controls":  Controls,
+		"defer":     Defer,
+		"disabled":  Disabled,
+		"multiple":  Multiple,
+		"readonly":  ReadOnly,
+		"required":  Required,
+		"selected":  Selected,
+	}
+
+	for name, fn := range cases {
+		t.Run(fmt.Sprintf("should output %v", name), func(t *testing.T) {
+			n := g.El("div", fn())
+			assert.Equal(t, fmt.Sprintf(`<div %v></div>`, name), n)
+		})
+	}
+}
+
+func TestSimpleAttributes(t *testing.T) {
+	cases := map[string]func(string) g.Node{
+		"accept":       Accept,
+		"autocomplete": AutoComplete,
+		"charset":      Charset,
+		"class":        Class,
+		"cols":         Cols,
+		"content":      Content,
+		"form":         FormAttr,
+		"height":       Height,
+		"href":         Href,
+		"id":           ID,
+		"lang":         Lang,
+		"max":          Max,
+		"maxlength":    MaxLength,
+		"min":          Min,
+		"minlength":    MinLength,
+		"name":         Name,
+		"pattern":      Pattern,
+		"preload":      Preload,
+		"placeholder":  Placeholder,
+		"rel":          Rel,
+		"rows":         Rows,
+		"src":          Src,
+		"style":        StyleAttr,
+		"tabindex":     TabIndex,
+		"target":       Target,
+		"title":        TitleAttr,
+		"type":         Type,
+		"value":        Value,
+		"width":        Width,
+	}
+
+	for name, fn := range cases {
+		t.Run(fmt.Sprintf(`should output %v="hat"`, name), func(t *testing.T) {
+			n := g.El("div", fn("hat"))
+			assert.Equal(t, fmt.Sprintf(`<div %v="hat"></div>`, name), n)
+		})
+	}
+}
diff --git a/html/elements.go b/html/elements.go
new file mode 100644
index 0000000..8045561
--- /dev/null
+++ b/html/elements.go
@@ -0,0 +1,447 @@
+// Package html provides common HTML elements and attributes.
+// See https://developer.mozilla.org/en-US/docs/Web/HTML/Element for a list of elements.
+// See https://developer.mozilla.org/en-US/docs/Web/HTML/Attributes for a list of attributes.
+package html
+
+import (
+	"fmt"
+	"io"
+
+	g "github.com/maragudk/gomponents"
+)
+
+func A(href string, children ...g.Node) g.NodeFunc {
+	return g.El("a", g.Attr("href", href), g.Group(children))
+}
+
+// Document returns an special kind of Node that prefixes its child with the string "<!doctype html>".
+func Document(child g.Node) g.NodeFunc {
+	return func(w io.Writer) error {
+		if _, err := w.Write([]byte("<!doctype html>")); err != nil {
+			return err
+		}
+		return child.Render(w)
+	}
+}
+
+// FormEl returns an element with name "form", the given action and method attributes, and the given children.
+func FormEl(action, method string, children ...g.Node) g.NodeFunc {
+	return g.El("form", g.Attr("action", action), g.Attr("method", method), g.Group(children))
+}
+
+func Img(src, alt string, children ...g.Node) g.NodeFunc {
+	return g.El("img", g.Attr("src", src), g.Attr("alt", alt), g.Group(children))
+}
+
+// Input returns an element with name "input", the given type and name attributes, and the given children.
+// Note that "type" is a keyword in Go, so the parameter is called typ.
+func Input(typ, name string, children ...g.Node) g.NodeFunc {
+	return g.El("input", g.Attr("type", typ), g.Attr("name", name), g.Group(children))
+}
+
+// Label returns an element with name "label", the given for attribute, and the given children.
+// Note that "for" is a keyword in Go, so the parameter is called forr.
+func Label(forr string, children ...g.Node) g.NodeFunc {
+	return g.El("label", g.Attr("for", forr), g.Group(children))
+}
+
+// Option returns an element with name "option", the given text content and value attribute, and the given children.
+func Option(text, value string, children ...g.Node) g.NodeFunc {
+	return g.El("option", g.Attr("value", value), g.Text(text), g.Group(children))
+}
+
+// Progress returns an element with name "progress", the given value and max attributes, and the given children.
+func Progress(value, max float64, children ...g.Node) g.NodeFunc {
+	return g.El("progress",
+		g.Attr("value", fmt.Sprintf("%v", value)),
+		g.Attr("max", fmt.Sprintf("%v", max)),
+		g.Group(children))
+}
+
+// Select returns an element with name "select", the given name attribute, and the given children.
+func Select(name string, children ...g.Node) g.NodeFunc {
+	return g.El("select", g.Attr("name", name), g.Group(children))
+}
+
+// Textarea returns an element with name "textarea", the given name attribute, and the given children.
+func Textarea(name string, children ...g.Node) g.NodeFunc {
+	return g.El("textarea", g.Attr("name", name), g.Group(children))
+}
+
+func Address(children ...g.Node) g.NodeFunc {
+	return g.El("address", children...)
+}
+
+func Area(children ...g.Node) g.NodeFunc {
+	return g.El("area", children...)
+}
+
+func Article(children ...g.Node) g.NodeFunc {
+	return g.El("article", children...)
+}
+
+func Aside(children ...g.Node) g.NodeFunc {
+	return g.El("aside", children...)
+}
+
+func Audio(children ...g.Node) g.NodeFunc {
+	return g.El("audio", children...)
+}
+
+func Base(children ...g.Node) g.NodeFunc {
+	return g.El("base", children...)
+}
+
+func BlockQuote(children ...g.Node) g.NodeFunc {
+	return g.El("blockquote", children...)
+}
+
+func Body(children ...g.Node) g.NodeFunc {
+	return g.El("body", children...)
+}
+
+func Br(children ...g.Node) g.NodeFunc {
+	return g.El("br", children...)
+}
+
+func Button(children ...g.Node) g.NodeFunc {
+	return g.El("button", children...)
+}
+
+func Canvas(children ...g.Node) g.NodeFunc {
+	return g.El("canvas", children...)
+}
+
+func Cite(children ...g.Node) g.NodeFunc {
+	return g.El("cite", children...)
+}
+
+func Code(children ...g.Node) g.NodeFunc {
+	return g.El("code", children...)
+}
+
+func Col(children ...g.Node) g.NodeFunc {
+	return g.El("col", children...)
+}
+
+func ColGroup(children ...g.Node) g.NodeFunc {
+	return g.El("colgroup", children...)
+}
+
+func Data(children ...g.Node) g.NodeFunc {
+	return g.El("data", children...)
+}
+
+func DataList(children ...g.Node) g.NodeFunc {
+	return g.El("datalist", children...)
+}
+
+func Details(children ...g.Node) g.NodeFunc {
+	return g.El("details", children...)
+}
+
+func Dialog(children ...g.Node) g.NodeFunc {
+	return g.El("dialog", children...)
+}
+
+func Div(children ...g.Node) g.NodeFunc {
+	return g.El("div", children...)
+}
+
+func Dl(children ...g.Node) g.NodeFunc {
+	return g.El("dl", children...)
+}
+
+func Embed(children ...g.Node) g.NodeFunc {
+	return g.El("embed", children...)
+}
+
+func FieldSet(children ...g.Node) g.NodeFunc {
+	return g.El("fieldset", children...)
+}
+
+func Figure(children ...g.Node) g.NodeFunc {
+	return g.El("figure", children...)
+}
+
+func Footer(children ...g.Node) g.NodeFunc {
+	return g.El("footer", children...)
+}
+
+func Head(children ...g.Node) g.NodeFunc {
+	return g.El("head", children...)
+}
+
+func Header(children ...g.Node) g.NodeFunc {
+	return g.El("header", children...)
+}
+
+func HGroup(children ...g.Node) g.NodeFunc {
+	return g.El("hgroup", children...)
+}
+
+func Hr(children ...g.Node) g.NodeFunc {
+	return g.El("hr", children...)
+}
+
+func HTML(children ...g.Node) g.NodeFunc {
+	return g.El("html", children...)
+}
+
+func IFrame(children ...g.Node) g.NodeFunc {
+	return g.El("iframe", children...)
+}
+
+func Legend(children ...g.Node) g.NodeFunc {
+	return g.El("legend", children...)
+}
+
+func Li(children ...g.Node) g.NodeFunc {
+	return g.El("li", children...)
+}
+
+func Link(children ...g.Node) g.NodeFunc {
+	return g.El("link", children...)
+}
+
+func Main(children ...g.Node) g.NodeFunc {
+	return g.El("main", children...)
+}
+
+func Menu(children ...g.Node) g.NodeFunc {
+	return g.El("menu", children...)
+}
+
+func Meta(children ...g.Node) g.NodeFunc {
+	return g.El("meta", children...)
+}
+
+func Meter(children ...g.Node) g.NodeFunc {
+	return g.El("meter", children...)
+}
+
+func Nav(children ...g.Node) g.NodeFunc {
+	return g.El("nav", children...)
+}
+
+func NoScript(children ...g.Node) g.NodeFunc {
+	return g.El("noscript", children...)
+}
+
+func Object(children ...g.Node) g.NodeFunc {
+	return g.El("object", children...)
+}
+
+func Ol(children ...g.Node) g.NodeFunc {
+	return g.El("ol", children...)
+}
+
+func OptGroup(children ...g.Node) g.NodeFunc {
+	return g.El("optgroup", children...)
+}
+
+func P(children ...g.Node) g.NodeFunc {
+	return g.El("p", children...)
+}
+
+func Param(children ...g.Node) g.NodeFunc {
+	return g.El("param", children...)
+}
+
+func Picture(children ...g.Node) g.NodeFunc {
+	return g.El("picture", children...)
+}
+
+func Pre(children ...g.Node) g.NodeFunc {
+	return g.El("pre", children...)
+}
+
+func Script(children ...g.Node) g.NodeFunc {
+	return g.El("script", children...)
+}
+
+func Section(children ...g.Node) g.NodeFunc {
+	return g.El("section", children...)
+}
+
+func Source(children ...g.Node) g.NodeFunc {
+	return g.El("source", children...)
+}
+
+func Span(children ...g.Node) g.NodeFunc {
+	return g.El("span", children...)
+}
+
+func StyleEl(children ...g.Node) g.NodeFunc {
+	return g.El("style", children...)
+}
+
+func Summary(children ...g.Node) g.NodeFunc {
+	return g.El("summary", children...)
+}
+
+func SVG(children ...g.Node) g.NodeFunc {
+	return g.El("svg", children...)
+}
+
+func Table(children ...g.Node) g.NodeFunc {
+	return g.El("table", children...)
+}
+
+func TBody(children ...g.Node) g.NodeFunc {
+	return g.El("tbody", children...)
+}
+
+func Td(children ...g.Node) g.NodeFunc {
+	return g.El("td", children...)
+}
+
+func TFoot(children ...g.Node) g.NodeFunc {
+	return g.El("tfoot", children...)
+}
+
+func Th(children ...g.Node) g.NodeFunc {
+	return g.El("th", children...)
+}
+
+func THead(children ...g.Node) g.NodeFunc {
+	return g.El("thead", children...)
+}
+
+func Tr(children ...g.Node) g.NodeFunc {
+	return g.El("tr", children...)
+}
+
+func Ul(children ...g.Node) g.NodeFunc {
+	return g.El("ul", children...)
+}
+
+func Wbr(children ...g.Node) g.NodeFunc {
+	return g.El("wbr", children...)
+}
+
+func Abbr(text string, children ...g.Node) g.NodeFunc {
+	return g.El("abbr", g.Text(text), g.Group(children))
+}
+
+func B(text string, children ...g.Node) g.NodeFunc {
+	return g.El("b", g.Text(text), g.Group(children))
+}
+
+func Caption(text string, children ...g.Node) g.NodeFunc {
+	return g.El("caption", g.Text(text), g.Group(children))
+}
+
+func Dd(text string, children ...g.Node) g.NodeFunc {
+	return g.El("dd", g.Text(text), g.Group(children))
+}
+
+func Del(text string, children ...g.Node) g.NodeFunc {
+	return g.El("del", g.Text(text), g.Group(children))
+}
+
+func Dfn(text string, children ...g.Node) g.NodeFunc {
+	return g.El("dfn", g.Text(text), g.Group(children))
+}
+
+func Dt(text string, children ...g.Node) g.NodeFunc {
+	return g.El("dt", g.Text(text), g.Group(children))
+}
+
+func Em(text string, children ...g.Node) g.NodeFunc {
+	return g.El("em", g.Text(text), g.Group(children))
+}
+
+func FigCaption(text string, children ...g.Node) g.NodeFunc {
+	return g.El("figcaption", g.Text(text), g.Group(children))
+}
+
+// H1 returns an element with name "h1", the given text content, and the given children.
+func H1(text string, children ...g.Node) g.NodeFunc {
+	return g.El("h1", g.Text(text), g.Group(children))
+}
+
+// H2 returns an element with name "h2", the given text content, and the given children.
+func H2(text string, children ...g.Node) g.NodeFunc {
+	return g.El("h2", g.Text(text), g.Group(children))
+}
+
+// H3 returns an element with name "h3", the given text content, and the given children.
+func H3(text string, children ...g.Node) g.NodeFunc {
+	return g.El("h3", g.Text(text), g.Group(children))
+}
+
+// H4 returns an element with name "h4", the given text content, and the given children.
+func H4(text string, children ...g.Node) g.NodeFunc {
+	return g.El("h4", g.Text(text), g.Group(children))
+}
+
+// H5 returns an element with name "h5", the given text content, and the given children.
+func H5(text string, children ...g.Node) g.NodeFunc {
+	return g.El("h5", g.Text(text), g.Group(children))
+}
+
+// H6 returns an element with name "h6", the given text content, and the given children.
+func H6(text string, children ...g.Node) g.NodeFunc {
+	return g.El("h6", g.Text(text), g.Group(children))
+}
+
+func I(text string, children ...g.Node) g.NodeFunc {
+	return g.El("i", g.Text(text), g.Group(children))
+}
+
+func Ins(text string, children ...g.Node) g.NodeFunc {
+	return g.El("ins", g.Text(text), g.Group(children))
+}
+
+func Kbd(text string, children ...g.Node) g.NodeFunc {
+	return g.El("kbd", g.Text(text), g.Group(children))
+}
+
+func Mark(text string, children ...g.Node) g.NodeFunc {
+	return g.El("mark", g.Text(text), g.Group(children))
+}
+
+func Q(text string, children ...g.Node) g.NodeFunc {
+	return g.El("q", g.Text(text), g.Group(children))
+}
+
+func S(text string, children ...g.Node) g.NodeFunc {
+	return g.El("s", g.Text(text), g.Group(children))
+}
+
+func Samp(text string, children ...g.Node) g.NodeFunc {
+	return g.El("samp", g.Text(text), g.Group(children))
+}
+
+func Small(text string, children ...g.Node) g.NodeFunc {
+	return g.El("small", g.Text(text), g.Group(children))
+}
+
+func Strong(text string, children ...g.Node) g.NodeFunc {
+	return g.El("strong", g.Text(text), g.Group(children))
+}
+
+func Sub(text string, children ...g.Node) g.NodeFunc {
+	return g.El("sub", g.Text(text), g.Group(children))
+}
+
+func Sup(text string, children ...g.Node) g.NodeFunc {
+	return g.El("sup", g.Text(text), g.Group(children))
+}
+
+func Time(text string, children ...g.Node) g.NodeFunc {
+	return g.El("time", g.Text(text), g.Group(children))
+}
+
+func TitleEl(title string, children ...g.Node) g.NodeFunc {
+	return g.El("title", g.Text(title), g.Group(children))
+}
+
+func U(text string, children ...g.Node) g.NodeFunc {
+	return g.El("u", g.Text(text), g.Group(children))
+}
+
+func Var(text string, children ...g.Node) g.NodeFunc {
+	return g.El("var", g.Text(text), g.Group(children))
+}
diff --git a/html/elements_test.go b/html/elements_test.go
new file mode 100644
index 0000000..e550bfe
--- /dev/null
+++ b/html/elements_test.go
@@ -0,0 +1,212 @@
+package html_test
+
+import (
+	"errors"
+	"fmt"
+	"testing"
+
+	g "github.com/maragudk/gomponents"
+	"github.com/maragudk/gomponents/assert"
+	. "github.com/maragudk/gomponents/html"
+)
+
+type erroringWriter struct{}
+
+func (w *erroringWriter) Write(p []byte) (n int, err error) {
+	return 0, errors.New("don't want to write")
+}
+
+func TestDocument(t *testing.T) {
+	t.Run("returns doctype and children", func(t *testing.T) {
+		assert.Equal(t, `<!doctype html><html></html>`, Document(g.El("html")))
+	})
+
+	t.Run("errors on write error in Render", func(t *testing.T) {
+		err := Document(g.El("html")).Render(&erroringWriter{})
+		assert.Error(t, err)
+	})
+}
+
+func TestFormEl(t *testing.T) {
+	t.Run("returns a form element with action and method attributes", func(t *testing.T) {
+		assert.Equal(t, `<form action="/" method="post"></form>`, FormEl("/", "post"))
+	})
+}
+
+func TestInput(t *testing.T) {
+	t.Run("returns an input element with attributes type and name", func(t *testing.T) {
+		assert.Equal(t, `<input type="text" name="hat">`, Input("text", "hat"))
+	})
+}
+
+func TestLabel(t *testing.T) {
+	t.Run("returns a label element with attribute for", func(t *testing.T) {
+		assert.Equal(t, `<label for="hat">Hat</label>`, Label("hat", g.Text("Hat")))
+	})
+}
+
+func TestOption(t *testing.T) {
+	t.Run("returns an option element with attribute label and content", func(t *testing.T) {
+		assert.Equal(t, `<option value="hat">Hat</option>`, Option("Hat", "hat"))
+	})
+}
+
+func TestProgress(t *testing.T) {
+	t.Run("returns a progress element with attributes value and max", func(t *testing.T) {
+		assert.Equal(t, `<progress value="5.5" max="10"></progress>`, Progress(5.5, 10))
+	})
+}
+
+func TestSelect(t *testing.T) {
+	t.Run("returns a select element with attribute name", func(t *testing.T) {
+		assert.Equal(t, `<select name="hat"><option value="partyhat">Partyhat</option></select>`,
+			Select("hat", Option("Partyhat", "partyhat")))
+	})
+}
+
+func TestTextarea(t *testing.T) {
+	t.Run("returns a textarea element with attribute name", func(t *testing.T) {
+		assert.Equal(t, `<textarea name="hat"></textarea>`, Textarea("hat"))
+	})
+}
+
+func TestA(t *testing.T) {
+	t.Run("returns an a element with a href attribute", func(t *testing.T) {
+		assert.Equal(t, `<a href="#">hat</a>`, A("#", g.Text("hat")))
+	})
+}
+
+func TestImg(t *testing.T) {
+	t.Run("returns an img element with href and alt attributes", func(t *testing.T) {
+		assert.Equal(t, `<img src="hat.png" alt="hat" id="image">`, Img("hat.png", "hat", g.Attr("id", "image")))
+	})
+}
+
+func TestSimpleElements(t *testing.T) {
+	cases := map[string]func(...g.Node) g.NodeFunc{
+		"address":    Address,
+		"article":    Article,
+		"aside":      Aside,
+		"audio":      Audio,
+		"blockquote": BlockQuote,
+		"body":       Body,
+		"button":     Button,
+		"canvas":     Canvas,
+		"cite":       Cite,
+		"code":       Code,
+		"colgroup":   ColGroup,
+		"data":       Data,
+		"datalist":   DataList,
+		"details":    Details,
+		"dialog":     Dialog,
+		"div":        Div,
+		"dl":         Dl,
+		"fieldset":   FieldSet,
+		"figure":     Figure,
+		"footer":     Footer,
+		"head":       Head,
+		"header":     Header,
+		"hgroup":     HGroup,
+		"html":       HTML,
+		"iframe":     IFrame,
+		"legend":     Legend,
+		"li":         Li,
+		"main":       Main,
+		"menu":       Menu,
+		"meter":      Meter,
+		"nav":        Nav,
+		"noscript":   NoScript,
+		"object":     Object,
+		"ol":         Ol,
+		"optgroup":   OptGroup,
+		"p":          P,
+		"picture":    Picture,
+		"pre":        Pre,
+		"script":     Script,
+		"section":    Section,
+		"span":       Span,
+		"style":      StyleEl,
+		"summary":    Summary,
+		"svg":        SVG,
+		"table":      Table,
+		"tbody":      TBody,
+		"td":         Td,
+		"tfoot":      TFoot,
+		"th":         Th,
+		"thead":      THead,
+		"tr":         Tr,
+		"ul":         Ul,
+	}
+
+	for name, fn := range cases {
+		t.Run(fmt.Sprintf("should output %v", name), func(t *testing.T) {
+			n := fn(g.Attr("id", "hat"))
+			assert.Equal(t, fmt.Sprintf(`<%v id="hat"></%v>`, name, name), n)
+		})
+	}
+}
+
+func TestSimpleVoidKindElements(t *testing.T) {
+	cases := map[string]func(...g.Node) g.NodeFunc{
+		"area":   Area,
+		"base":   Base,
+		"br":     Br,
+		"col":    Col,
+		"embed":  Embed,
+		"hr":     Hr,
+		"link":   Link,
+		"meta":   Meta,
+		"param":  Param,
+		"source": Source,
+		"wbr":    Wbr,
+	}
+
+	for name, fn := range cases {
+		t.Run(fmt.Sprintf("should output %v", name), func(t *testing.T) {
+			n := fn(g.Attr("id", "hat"))
+			assert.Equal(t, fmt.Sprintf(`<%v id="hat">`, name), n)
+		})
+	}
+}
+
+func TestTextElements(t *testing.T) {
+	cases := map[string]func(string, ...g.Node) g.NodeFunc{
+		"abbr":       Abbr,
+		"b":          B,
+		"caption":    Caption,
+		"dd":         Dd,
+		"del":        Del,
+		"dfn":        Dfn,
+		"dt":         Dt,
+		"em":         Em,
+		"figcaption": FigCaption,
+		"h1":         H1,
+		"h2":         H2,
+		"h3":         H3,
+		"h4":         H4,
+		"h5":         H5,
+		"h6":         H6,
+		"i":          I,
+		"ins":        Ins,
+		"kbd":        Kbd,
+		"mark":       Mark,
+		"q":          Q,
+		"s":          S,
+		"samp":       Samp,
+		"small":      Small,
+		"strong":     Strong,
+		"sub":        Sub,
+		"sup":        Sup,
+		"time":       Time,
+		"title":      TitleEl,
+		"u":          U,
+		"var":        Var,
+	}
+
+	for name, fn := range cases {
+		t.Run(fmt.Sprintf("should output %v", name), func(t *testing.T) {
+			n := fn("hat", g.Attr("id", "hat"))
+			assert.Equal(t, fmt.Sprintf(`<%v id="hat">hat</%v>`, name, name), n)
+		})
+	}
+}