This guide establishes the standard for creating reusable components in tinywasm/components.
Each component must reside in its own folder within tinywasm/components and consist of at least 2 files:
tinywasm/components/
└── mycomponent/
├── mycomponent.go # Shared struct, Render(), OnMount()
├── mycomponent.css # Component-scoped styles
├── mycomponent_test.go # Tests
└── ssr.go # Backend only: CSS embed, IconSvg() — build tag !wasm
There is NO
front.go. WASM interactivity lives inmycomponent.goviaOnMount(). The build system separates concerns via build tags onssr.go, not by splitting files.
- All colors MUST use
--color-*CSS custom properties fromtinywasm/domtheme. - Never hardcode hex values. Always provide a fallback:
var(--color-secondary, #00ADD8). - Spacing MUST use
--mag-pri,--mag-sec,--mag-cuavariables. - CSS class names MUST be prefixed with the component name to avoid collisions:
mycomponent-*. - CSS lives in
<component>.css, embedded inssr.govia//go:embed. - Do NOT create or embed form-related CSS — use
tinywasm/form.
Contains the struct definition, Render() (shared SSR + WASM), and OnMount() (WASM only — no build tag needed; dead code eliminated by TinyGo).
Always embed dom.Element as a VALUE, never as a pointer.
// ✅ CORRECT — value embed, zero GC overhead, no nil risk
type MyComponent struct {
dom.Element
Title string
}
// ❌ WRONG — pointer embed causes 2 heap allocations, requires nil-guard,
// risks nil panic in WASM, wastes GC cycles in TinyGo
type MyComponent struct {
*dom.Element
}Why value embedding:
- TinyGo's GC is conservative and simple — fewer heap objects = fewer GC pauses
- One allocation instead of two (struct + Element separately)
- Better cache locality — Element fields are contiguous with the struct
- No nil-guard boilerplate, no nil panic risk in production WASM
package mycomponent
import "github.com/tinywasm/dom"
type MyComponent struct {
dom.Element // value embed — never pointer
Title string
}
func (c *MyComponent) Render() *dom.Element {
return dom.Div().
Class("mycomponent").
Text(c.Title)
}
// OnMount wires events after the component is injected into the DOM.
// Called automatically by tinywasm/dom — no build tag needed.
// TinyGo eliminates this as dead code on SSR builds.
func (c *MyComponent) OnMount() {
id := c.GetID()
if el, ok := dom.Get(id); ok {
el.On("click", func(e dom.Event) {
// handle click
})
}
}CRITICAL: This file MUST have the //go:build !wasm build tag.
SVG strings and embedded CSS are dead weight in the WASM binary — keep them here.
//go:build !wasm
package mycomponent
import _ "embed"
//go:embed mycomponent.css
var css string
func (c *MyComponent) RenderCSS() string {
return css
}El framework inyecta el sprite SVG directamente en el <body> del HTML en tiempo de servidor. No existe una URL pública /assets/icons.svg — el sprite vive solo en memoria e inline en el HTML.
La cadena es: IconSvg() en ssr.go → sprite generado en memoria → inyectado inline en HTML → <svg><use href="#id"> en Render() resuelve sin ningún request de red.
MANDATORY:
IconSvg()MUST be inssr.go(//go:build !wasm). SVG strings are dead code on WASM — never define icons in the main file.
MANDATORY: All paths and shapes MUST include
fill="currentColor"(orstroke="currentColor"if stroke-based) so CSS can control the icon color viafillorcoloron any ancestor.
ssr.go — registrar el icono:
func (c *MyComponent) IconSvg() map[string]string {
return map[string]string{
// Do NOT include the wrapping <svg> tag — the system adds it.
// Only internal content: paths, circles, etc.
// Default viewBox is "0 0 16 16". Include viewBox="..." in string to override.
"my-icon-id": `<path fill="currentColor" d="..." />`,
}
}component.go Render() — referenciar el icono con <svg><use>:
dom.Svg(dom.Use().Attr("href", "#my-icon-id")).Class("my-icon")component.css — controlar apariencia desde CSS:
.my-icon {
width: 1em;
height: 1em;
fill: currentColor; /* hereda el color del texto del ancestro */
transition: transform 0.2s;
}Tests run on the backend (no build tag needed — default is !wasm when not targeting WASM).
Call Render().RenderHTML() and assert on the HTML string.
package mycomponent
import (
"strings"
"testing"
)
func TestMyComponent_Render(t *testing.T) {
c := &MyComponent{Title: "Hello"}
html := c.Render().RenderHTML()
if !strings.Contains(html, "mycomponent") {
t.Error("expected mycomponent class")
}
if !strings.Contains(html, "Hello") {
t.Error("expected title text")
}
}tinywasm/site collects RenderCSS() and IconSvg() from all registered components (SSR build, !wasm). The WASM client receives only the struct logic and Render()/OnMount() — CSS and SVG strings never reach the binary.