Ultra-minimal DOM & event toolkit for Go (TinyGo WASM-optimized).
tinywasm/dom provides a minimalist, WASM-optimized way to interact with the browser DOM in Go, avoiding the overhead of the standard library and syscall/js exposure. It is designed specifically for TinyGo applications where binary size and performance are critical.
- Lifecycle & DOM API:
Render,Append,Update,Get,OnHashChange - Void Element Fix: Correctly renders
<br>,<img>,<hr>without closing tags - TinyGo Optimized: Avoids heavy standard library packages to keep WASM binaries <500KB
- Direct DOM Manipulation: No Virtual DOM overhead. You control the updates.
- ID-Based Caching: Efficient element lookup and caching strategy
- Lifecycle Hooks:
OnMount,OnUpdate,OnUnmountfor fine-grained control
go get github.com/tinywasm/domFor a complete example including Elm architecture (Dynamic Components) and Static Components, see:
π tinywasm/html β web/client.go
That file uses tinywasm/html for element builders and tinywasm/dom for lifecycle (Render, Update, Append).
tinywasm/dom focuses on DOM manipulation and component lifecycles. HTML element builders have been moved to their own packages:
- tinywasm/html β HTML element builders (Div, Span, Nav...)
- tinywasm/svg β SVG builders + icon sprite system
- tinywasm/image β Image element builders
Components can implement optional lifecycle interfaces:
type MyComponent struct {
dom.Element
data []string
}
// Called after component is mounted to DOM
func (c *MyComponent) OnMount() {
c.data = fetchData()
c.Update()
}
// Called after re-render (dom.Update)
func (c *MyComponent) OnUpdate() {
fmt.Println("Component updated")
}
// Called before component is removed
func (c *MyComponent) OnUnmount() {
// Cleanup resources
}All components must implement:
type Component interface {
GetID() string
SetID(string)
String() string // OR Render() *Element
Children() []Component
}Two rendering options:
String() string- For static components (smaller binary)Render() *dom.Element- For dynamic components (type-safe, composable)
Components can implement either or both. DOM checks Render() first, falls back to String().
Choose the right rendering method for each component:
| Component Type | Method | Benefit |
|---|---|---|
| Static (no interactivity) | String() string |
Smaller binary, less overhead |
| Dynamic (interactive, state) | Render() *dom.Element |
Type-safe, composable, fluent API |
See the implementation examples in web/client.go to see both approaches in action.
Components can contain child components:
type MyList struct {
dom.Element
items []dom.Component
}
func (c *MyList) Children() []dom.Component {
return c.items
}
func (c *MyList) Render() *dom.Element {
list := html.Div()
for _, item := range c.items {
list.Add(item) // Components can be children
}
return list
}When you call dom.Render("app", myList), the library will:
- Render the HTML
- Call
OnMount()forMyList - Recursively call
OnMount()for allitems
The same recursion applies to cleanup, ensuring all event listeners are cleaned up when a parent is replaced.
Event handling is integrated directly into the Builder API via On(eventType, handler).
// Rendering
dom.Render(parentID, component) // Replace parent's content
dom.Append(parentID, component) // Append after last child
dom.Update(component) // Re-render in place
dom.Get(id) // Get a DOM Reference (value, focus, events)
// Routing (hash-based)
dom.OnHashChange(handler) // Listen to hash changes
dom.GetHash() // Get current hash
dom.SetHash(hash) // Set hashEmbedding dom.Element provides these methods automatically:
type Counter struct {
dom.Element
count int
}
// Chainable helpers
counter.Update() // Trigger re-render
counter.GetID() // Get unique ID
counter.SetID("my-id") // Set custom IDFor more detailed information, please refer to the documentation in the docs/ directory:
- Architecture & Builder API Guide: Comprehensive guide covering the isomorphic component model, the JSX-like builder, event handling, and optimization strategies for TinyGo.
- Agent Guide: Constraints and rules for agents (and humans) adding or modifying functionality β build split, error handling, naming, testing, and DOM boundary decisions.
-
β Major API Redesign - Builders moved to separate packages
-
β Interface Standardized -
RenderHTML() stringβString() string(fmt.Stringer) -
β Internal Privatization - Cleaned up public API (privatized
EventHandler, etc.) -
β Void Element Rendering - Correct HTML for
<br>,<img>,<hr> -
β Auto-ID Generation - Simplified IDs without
auto-prefix -
β Void Element Rendering - Correct HTML for
<br>,<img>,<hr> -
β Fluent Builder API - Chainable methods (
html.Div().ID("x").Class("y")) -
β Hybrid rendering - Choose DSL or string HTML per component
-
β Lifecycle hooks -
OnMount,OnUpdate,OnUnmount -
β Auto-ID generation - All components get unique IDs automatically
Binary Size (TinyGo WASM):
- Simple counter app: ~35KB (compressed)
- Todo list with 10 components: ~120KB (compressed)
- Full application: <500KB (compressed)
Compared to standard library approach: 60-80% smaller binaries.
MIT