Type-safe HTML for Go: no templates, no code generation, no build step.
elem-go lets you build HTML views in pure Go: every element is a Go function and every attribute a typed constant, so typos fail at compile time instead of at runtime in production. And because views are plain Go code rather than a separate template language, the tools you already use work on them unchanged: autocomplete, refactoring, tests, the debugger.
- Type-safe constructors for nearly every standard HTML5 element and attribute.
- Utilities like
IfandTransformEachfor conditional and list rendering. - Automatic HTML escaping of text content (use
Rawto opt out). - Zero runtime dependencies.
- Inline CSS styling with the styles subpackage.
- Advanced CSS features (pseudo-classes, animations, media queries) with
StyleManager. - htmx attribute helpers in the htmx subpackage.
To install elem-go, use go get:
go get github.com/chasefleming/elem-goImport the elem package in your Go code:
import (
"github.com/chasefleming/elem-go"
"github.com/chasefleming/elem-go/attrs"
"github.com/chasefleming/elem-go/styles"
)Here's an example of creating a <div> element with nested <h1>, <h2>, and <p> elements using elem-go:
content := elem.Div(attrs.Props{
attrs.ID: "container",
attrs.Class: "my-class",
},
elem.H1(nil, elem.Text("Hello, Elem!")),
elem.H2(nil, elem.Text("Subheading")),
elem.P(nil, elem.Text("This is a paragraph.")),
)Calling the .Render() method on this element produces the following HTML:
<div id="container" class="my-class">
<h1>Hello, Elem!</h1>
<h2>Subheading</h2>
<p>This is a paragraph.</p>
</div>The attrs subpackage provides type-safe attribute functions that ensure you're setting valid attributes for your elements. This helps eliminate potential issues at runtime due to misspelled or unsupported attribute names.
For boolean attributes like checked and selected, assign the value "true" or "false". When set to "true", the attribute renders without an explicit value:
// Using boolean attributes
checkbox := elem.Input(attrs.Props{
attrs.Type: "checkbox",
attrs.Checked: "true", // This will render as <input type="checkbox" checked>
})For setting styles, the styles subpackage enables you to create style objects and convert them to inline CSS strings:
// Define a style
buttonStyle := styles.Props{
styles.BackgroundColor: "blue",
styles.Color: "white",
}
// Convert style to inline CSS and apply it
button := elem.Button(
attrs.Props{
attrs.Style: buttonStyle.ToInline(),
},
elem.Text("Click Me"),
)See the complete list of supported attributes in the attrs package, and for a full overview of style properties and information on using the styles subpackage, see the styles README.
The .Render() method converts an element tree into an HTML string, ready to serve to a browser.
html := content.Render()NOTE: When using an element, this method automatically includes a preamble in the rendered HTML, ensuring compliance with modern web standards.
For more control over rendering, such as disabling the <!DOCTYPE html> preamble, use RenderWithOptions with a RenderOptions struct:
options := elem.RenderOptions{DisableHtmlPreamble: true}
htmlString := myHtmlElement.RenderWithOptions(options)The TransformEach function turns a slice of data into a slice of elements:
items := []string{"Item 1", "Item 2", "Item 3"}
liElements := elem.TransformEach(items, func(item string) elem.Node {
return elem.Li(nil, elem.Text(item))
})
ulElement := elem.Ul(nil, liElements...)In this example, we transformed a slice of strings into a list of li elements and then wrapped them in a ul element.
elem-go provides a utility function If for conditional rendering of elements.
isAdmin := true
adminLink := elem.A(attrs.Props{attrs.Href: "/admin"}, elem.Text("Admin Panel"))
guestLink := elem.A(attrs.Props{attrs.Href: "/login"}, elem.Text("Login"))
content := elem.Div(nil,
elem.H1(nil, elem.Text("Dashboard")),
elem.If(isAdmin, adminLink, guestLink),
)In this example, if isAdmin is true, the Admin Panel link is rendered. Otherwise, the Login link is rendered.
elem-go provides a specialized node None that implements the Node interface but produces no output, for cases where a condition should render nothing.
showWelcomeMessage := false
welcomeMessage := elem.Div(nil, elem.Text("Welcome to our website!"))
content := elem.Div(nil,
elem.If[elem.Node](showWelcomeMessage, welcomeMessage, elem.None()),
)In this example, welcomeMessage is rendered only if showWelcomeMessage is true. If it's false, None is rendered instead, which produces no visible output.
Additionally, None can be used to create an empty element, as in elem.Div(nil, elem.None()), which results in <div></div>.
elem-go provides constructor functions for HTML elements:
- Document Structure:
Html,Head,Body,Title,Link,Meta,Style,Base - Text Content:
H1,H2,H3,H4,H5,H6,P,Blockquote,Pre,Code,I,Br,Wbr,Hr,Small,Q,Cite,Abbr,Data,Time,Var,Samp,Kbd - Sectioning & Semantic Layout:
Article,Aside,FigCaption,Figure,Footer,Header,Hgroup,Main,Mark,Nav,Search,Section - Form Elements:
Form,Input,Textarea,Button,Select,Optgroup,Option,Label,Fieldset,Legend,Datalist,Meter,Output,Progress - Interactive Elements:
Details,Dialog,Menu,Summary - Grouping Content:
Div,Span,Li,Ul,Ol,Dl,Dt,Dd - Tables:
Table,Caption,Col,Colgroup,Tr,Td,Th,TBody,THead,TFoot - Hyperlinks and Multimedia:
Img,Picture,Map,Area - Embedded Content:
Audio,Canvas,Embed,Iframe,Object,Source,Track,Video - Script-supporting Elements:
Script,Noscript,Slot,Template - Inline Semantic:
A,Strong,Em,Code,I,B,Bdi,Bdo,U,S,Sub,Sup,Del,Dfn,Ins,Ruby,Rt,Rp
The Raw function inserts raw HTML verbatim into your document structure:
rawHTML := `<div class="custom-html"><p>Custom HTML content</p></div>`
content := elem.Div(nil,
elem.H1(nil, elem.Text("Welcome to Elem-Go")),
elem.Raw(rawHTML), // Inserting the raw HTML
elem.P(nil, elem.Text("More content here...")),
)
htmlOutput := content.Render()
// Output: <div><h1>Welcome to Elem-Go</h1><div class="custom-html"><p>Custom HTML content</p></div><p>More content here...</p></div>NOTE: If you are passing HTML from an untrusted source, make sure to sanitize it to prevent potential security risks such as Cross-Site Scripting (XSS) attacks.
Apart from standard elements, elem-go also allows you to insert HTML comments using the Comment function:
comment := elem.Comment("Section: Main Content Start")
// Generates: <!-- Section: Main Content Start -->When you pass content to the Script function, elem-go automatically escapes the sequences that would otherwise terminate the <script> element early (</script>, <script, and <!--, all case-insensitive) by replacing the leading < with its \x3C escape, as required by the HTML specification. This keeps inline scripts from accidentally breaking out of their element.
script := elem.Script(nil, elem.Raw(`alert("</script>")`))
// Renders: <script>alert("\x3C/script>")</script>The Fragment function allows you to group multiple elements together without adding an extra wrapper element to the DOM. This is particularly useful when you want to merge multiple nodes into the same parent element without any additional structure.
nodes := []elem.Node{
elem.P(nil, elem.Text("1")),
elem.P(nil, elem.Text("2")),
}
content := elem.Div(nil,
elem.P(nil, elem.Text("0")),
elem.Fragment(nodes...),
elem.P(nil, elem.Text("3")),
)Here, the nodes are inserted directly into the parent div with no additional wrapper elements in the output.
When using attributes that require JSON strings or special characters (like quotes), make sure to wrap these strings in single quotes. This prevents the library from adding extra quotes around your value. For example:
content := elem.Div(attrs.Props{
attrs.ID: "my-div",
attrs.Class: "special 'class'",
attrs.Data: `'{"key": "value"}'`,
}, elem.Text("Content"))For advanced CSS styling, including animations, pseudo-classes, and responsive design via media queries, use StyleManager from the styles subpackage. It lets you create and manage complex CSS programmatically, with the same type safety as the rest of elem-go.
See the StyleManager documentation for details.
The htmx subpackage provides typed helpers for htmx attributes, so you can build dynamic server-rendered pages without writing JavaScript. It targets htmx 2.x, with deprecated constants preserved for code written against htmx 1.x.
For hands-on sample implementations, see the examples/ folder.
Dive deeper into elem-go with these tutorials and guides:
- Building a Counter App with htmx, Go Fiber, and elem-go
- Building a Go Static Site Generator Using elem-go
Contributions are welcome! If you have ideas for improvements or new features, please open an issue or submit a pull request.
This project is licensed under the MIT License - see the LICENSE file for details.
