This guide establishes the standard for creating reusable components in tinywasm/components.
Component names must be composed of at least two words — single-word names are reserved for primitive builder functions in tinywasm/dom (e.g. Div, Button, Input).
✅ ThemeSwitch NavBar DataTable UserCard SearchBox
❌ Switch Nav Table Card Search
This applies to both the Go struct name (ThemeSwitch) and the folder/package name (themeswitch). Two-word names make it immediately clear that something is a reusable component rather than a dom primitive or a stdlib type.
Each component must reside in its own folder within tinywasm/components and consist of at least 2 files:
tinywasm/components/
└── themeswitch/
├── themeswitch.go # Shared struct, Render(), OnMount()
├── themeswitch.css # Component-scoped styles
├── themeswitch_test.go # Tests
└── ssr.go # Backend only: CSS embed, IconSvg() — build tag !wasm
There is NO
front.go. WASM interactivity lives inthemeswitch.goviaOnMount(). The build system separates concerns via build tags onssr.go, not by splitting files.
- All colors MUST use
--color-*CSS custom properties exposed bytinywasm/dom's default theme (seedom/theme.css, injected as:root { … }byassetmin). - Never hardcode hex values. Use the token directly without fallback —
var(--color-secondary), notvar(--color-secondary, #00ADD8). Fallbacks break reusability: a component with a hardcoded fallback stops following the active theme when an app overrides it. - Spacing MUST use
--mag-pri,--mag-sec,--mag-cuavariables. - A component MUST NOT declare its own
:root { … }block. Theme tokens are global state owned by the app (ortinywasm/dom's default). Component CSS only consumes them. - 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.
Available tokens (from dom/theme.css):
--color-primary, --color-secondary, --color-tertiary, --color-quaternary
--color-gray, --color-selection, --color-hover, --color-success, --color-error
--menu-width-collapsed, --menu-width-expanded
--title-height, --content-height, --controls-height
--mag-pri, --mag-sec, --mag-cua
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
}RenderCSS() ships component-scoped CSS. assetmin extracts it (via the CSSProvider interface at runtime, or via AST when declared as a free RenderCSS() function) and routes it to the middle slot of <head>.
Do NOT declare a RootCSS() function in a component package. RootCSS() is reserved for the app or tinywasm/dom (single-override rule — third-party RootCSS() is silently ignored by assetmin with a warning). See assetmin/docs/SSR.md.
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")
}
}Components are consumed by the asset pipeline (tinywasm/assetmin) in two ways:
- Runtime registration —
am.RegisterComponents(myComponent)inspects the struct forCSSProvider,JSProvider,IconSvgProvider, andHTMLProviderinterfaces and routes the returned content into the bundle. This is how live components register themselves at server startup. - AST extraction — for static modules that ship assets without instantiation,
assetminparsesssr.golooking for free functions namedRenderCSS,RenderJS,RenderHTML,IconSvg, andRootCSS. Component packages typically use the runtime path (above); standalone modules use the AST path.
Either way: the WASM client receives only struct logic and Render()/OnMount(). CSS and SVG strings stay on the SSR side, gated by the !wasm build tag, and never reach the binary.
Slot routing reminder:
- Component
RenderCSS()→middleslot (loaded after the document:root). - App's
RootCSS()(ordom's default) →openslot. - Root project's
RenderCSS()→closeslot (last word, can override anything).