The component layer ¶
Write UI in .templ files with typed Go props and explicit slots. Generate the Go files with your project’s Cicadel CLI, then render them from HTTP handlers. Start with the component walkthrough.
Ask your agent ¶
Read https://cicadel.fauzaanu.com/en/dev/topics/component-ui/index.md.
Build a reusable component for [purpose] with typed props and explicit slots.
Show the .go contract, .templ markup and handler call. Use escaped text,
rebuild the generated UI, and test valid and invalid props.Authoring and public contracts ¶
Keep props.go and card.templ together in a Go package, commonly beneath an
app’s templates/cards/ directory. Private bodies and reusable page composition
may also live alongside the owning handlers, as in the articles example. Imports
are ordinary Go module imports and aliases; no filesystem-based name resolution
or global registry occurs. Exported contracts have appname:component names.
package cards
import "github.com/fauzaanu/cicadel/ui"
type Props struct {
Title string
Compact bool
}
type Slots struct {
Children ui.Content
Footer ui.Content
}
var Card = ui.Definition[Props, Slots]{
Name: "cards:card",
Source: "templates/cards/card.templ",
Validate: func(p Props, s Slots) error {
if err := ui.Required("Title", p.Title); err != nil { return err }
if s.Children.Empty() { return ui.Required("Children slot", "") }
return nil
},
Body: card,
}package cards
templ card(p Props, s Slots) {
<section>
<h2>{ p.Title }</h2>
@s.Children
if !s.Footer.Empty() {
<footer>@s.Footer</footer>
}
</section>
}content := cards.Card.Call(
cards.Props{Title: "Welcome"},
cards.Slots{Children: ui.Text("Hello"), Footer: ui.Text("Thanks")},
)
err := (ui.Renderer{}).HTML(r.Context(), w, 200, content)
if err != nil && !ui.Committed(err) { response.Error(w, 500) }
// Log errors privately; do not send diagnostic details to public clients.ui.Content is an opaque HTML fragment, never a string or attribute value.
ui.Text escapes strings; ui.Group composes fragments; ui.Fragment adapts a
trusted compiled component. Content{} is empty; ui.Text("") is a supplied empty
text value, so Empty() reports presence rather than rendered text length.
Use a named Children field for default child content, with other fields for
named slots. ui.NoSlots declares no slots. There is one model: explicit fields,
not templ’s implicit { children... } context transport (rejected by the compiler).
Use @someContent or @Definition.Call(props, slots) in markup.
Go checks field names, types, function arguments, imports, and slot compatibility.
Omitted struct fields have Go zero values. Validate declares required values and
slot presence; it runs before that component’s body and before HTTP commitment.
Required does not mean every zero value is invalid: the author specifies the
semantic rule. Defaults, if provided, runs first on a local props copy. Use
pointer/option fields when omitted and explicitly zero/false must differ; do not
silently treat all zero values as missing. Both hooks must be pure and must not
mutate referenced caller data. Override bodies retain the original default and
validation hooks. Exported definition variables are configuration values: treat
them as immutable; copy before customizing.
Dependencies and request data are explicit typed props, function parameters, or
factory arguments. There are no context processors or implicit request maps.
A nested body sees only its Go lexical scope and declared inputs. Request
context.Context carries cancellation and private render bookkeeping; business
services should be explicit typed inputs. Ordinary Go if, switch, for, and
helpers support variants and collections. All generated markup functions,
including private bodies and recursive calls, receive render frames.
Safety and rendering boundary ¶
- Text expressions and string attributes are escaped by templ across nested
components. Fragment slots are HTML-body values.
titleandtextareapermit only text expressions/text, not component insertion. Markup mismatched closing tags and unsupported context composition are compile errors. - Dynamic URL attributes use templ’s contextual URL sanitization; unsafe values
become
about:invalid#TemplFailedSanitizationURL. Useui.URLwhen the caller needs an error instead. It allows relative URLs andhttp,https,mailto, andtel; rejects control characters, backslashes, whitespace padding, protocol-relative URLs, and other schemes. This is not an origin allowlist. ui.Mergeis the forwarding boundary. Pass orderedui.Attributesgroups; names must be lowercase. Later values win, except classes merge unique tokens in first-seen order. Known boolean attributes require bools (false omits them). Text,aria-*, anddata-*values require strings; use"false"for ARIA false. URL-like attributes also passui.URL. Output attribute order is deterministic. Merge before spreading and do not duplicate those keys as literal attributes.- Forwarding is allowlisted. Events, inline style,
srcdoc,srcset, namespaces, arbitrary custom behavior attributes such ashx-get, and unknown standard attributes are rejected. Explicit attributes in trusted markup are a separate authoring decision; contextual URL escaping still applies. Applications may extend a typed component’s declared API rather than blindly forward everything. - Inline script/CSS templates, script/style elements, event-handler attributes,
dynamic attribute names, and active foreign/embedded content (
svg,math,iframe,object,embed, and obsolete raw-text elements) are outside the initial source contract and rejected. Use explicit static stylesheet links. Client enhancement adapters and active content support are future capabilities, not implied by composition. - Cicadel supplies no string-to-trusted-HTML/URL/JS conversion.
ui.Fragmentis a narrow adapter for trusted Go implementations, which must preserve HTML safety. Do not call upstreamtempl.Raw,SafeURL, or write untrusted strings directly. Components/packages execute trusted Go code, not a sandbox; they can bypass any framework contract deliberately. Review them like other dependencies.
Renderer.Render returns bytes only after success. Renderer.HTML renders first,
then sets content type/status and writes. A render error leaves the HTTP writer
untouched. A write failure after commitment is *ui.CommitError;
ui.Committed(err) distinguishes it. Log that error, never try a second response.
The caller must supply an uncommitted response writer. No streaming/flush API is
supported through Renderer; direct low-level component Render calls do not
provide HTTP atomicity. Go panics remain panics and are handled by the host’s
existing recovery middleware, not converted into validation failures.
Defaults are 64 render frames and 4 MiB output; override with
Renderer{MaxDepth: ..., MaxBytes: ...}. Frames include contract calls and markup
bodies. Cancellation is checked at component boundaries and buffered writes.
Arbitrary Go helpers must cooperate with context; the engine cannot preempt an
infinite Go computation. The output bound excludes caller-owned props and
intermediate Go expressions. No shared compile/render cache owns request values,
children, context, or output. Generated literal writes ignore upstream development-mode environment variables and never consult source files. Generated code/literals are immutable; Cicadel uses
request-owned buffers, deliberately bypassing templ’s global buffer pool, which
retains an underlying writer. Definitions/registries may be shared; the caller
must not mutate props, slices, maps, or closures while a render uses them.
Installable libraries and overrides ¶
App.Components []ui.Export explicitly registers Definition.Export(version).
The app owns the namespace, and duplicate names, invalid exports, or nil bodies
fail construction. Manifest / Requires provide app API and dependency-version
checks; Go modules select actual code. Component contract versions are positive
integers, independent of module versions. Nothing is activated by importing a
package or embedding source alone.
App.ComponentAssets lists required files in App.Resources; construction checks
they exist and are files. Serving is still an explicit route/static-host choice.
Compiled Go embeds markup; go:embed packages static resources and may package
sources for inspection. Runtime does not read .templ files or the working directory.
The local project app may supply ComponentOverrides. No package or manifest
app can override another package implicitly. One override wins over the original;
duplicate overrides fail. It must retain the exact named Go props/slots types and
contract version. The replacement supplies only the body/source: original default
and validation behavior remains authoritative. This prevents silent required-prop
or required-slot weakening. Semantic HTML behavior still requires application tests.
Build a registry with cicadel.ComponentRegistry(apps), resolve a definition with
ui.Resolve(registry, cards.Card), and pass that resolved value into page/route
factories. cicadel.New also validates and exposes Application.Components.
Calling an imported definition directly deliberately uses that definition; there
is no hidden lookup that changes its meaning. This explicit injection makes
project overrides reviewable and separate hosts independent.
The independent cards module includes a compiled
body, required child slot, optional footer, forwarded attributes, embedded source
and CSS, app manifest, and HTTP tests. It has no mandatory design-system dependency.
The generated consumer test publishes a
unique test version through a local Go proxy, performs real go get, installs it
into a fresh starter, explicitly overrides it, checks escaping and concurrency,
and runs compiled HTTP tests plus a production executable outside the source tree.
This is verified module installation, not a claim that the example is publicly
published. The local module’s framework replace is development-only.
Exact authoring and verification loop ¶
Generated projects pin the CLI through a Go tool directive. After go mod tidy:
go doc example.com/your-library.Props
# Edit the props/slots types and .templ body.
go tool cicadel ui css install # once, when using Tailwind
go tool cicadel ui build
go tool cicadel ui check --json
go test -run '^TestMyPage$' ./apps/myapp
go test -race ./...
go run manage.go check
go build -trimpath -o bin/server .In the framework checkout use go run ./cmd/cicadel ui build and go run ./cmd/cicadel ui check. For an existing
project add the matching CLI with go get -tool github.com/fauzaanu/cicadel/cmd/cicadel@VERSION.
The tool’s compiler and runtime must be from the same framework dependency graph.
generate parses all module sources before writing outputs. It writes deterministic
*_templ.go files atomically per file, including a source hash and compiler version;
commit them. build generates markup and builds configured Tailwind CSS; generate remains markup-only.
check leaves generated files unchanged: it detects missing/stale/orphan outputs,
checks configured CSS freshness, and
compiles all Go packages/tests (go test -c -o /dev/null ./..., using the platform null device). It does not execute tests,
start a web server, or evaluate arbitrary app factories. manage.go check constructs
and validates the actual installed registry/resources; host-specific database setup
still follows that host’s existing construction rules. Production cicadel.New
validates registered libraries, assets, and overrides before serving. Run both
source and host validation in CI. A plain go build cannot detect that an ignored
.templ source changed: stale checking is a required build step.
Automatic compilation and executable restart are available through go tool cicadel dev; see the foundation workflow. In-process hot
replacement is not supported. Manual build/check and restart of go run manage.go runserver remain supported. Failed generation leaves existing outputs intact for
syntax failures, and a failed development build keeps the working server running.
Concurrent generators in one checkout are unsupported. Nested Go
modules need their own invocation; .git/hidden directories, vendor, and testdata
are not compiled as application sources. Orphan generated files must be removed
explicitly, never silently adopted.
Commands exit 0 on success and 1 on failures, including invalid arguments. --json
emits a JSON array of diagnostics to stdout (empty on success); a human error is
also printed to stderr by the CLI. Fields: code, message, file, line,
column, stack. Positions are one-based. Stable IDs include UI_PARSE,
UI_SYNTAX, UI_GENERATE, UI_TYPE, UI_STALE, UI_ORPHAN, UI_BUILD,
UI_NAMESPACE, UI_COLLISION, UI_UNKNOWN, UI_CONTRACT, UI_ATTRIBUTE,
UI_URL, UI_RENDER, UI_LIMIT, the Tailwind UI_CSS_* errors, and UI_WRITE (committed transport failure).
Runtime diagnostics implement error/Unwrap and can be JSON-encoded. Nested
errors retain their deepest component stack. Source parse/render expression
locations are precise; Go type errors map to the owning .templ declaration and
preserve the exact generated Go position/message for detailed diagnosis. Plain Go
errors retain their Go source position. Error messages may contain input values;
keep them in development tooling/private logs.
Forms, application integration, and migration ¶
ui/layout.Page provides an optional minimal document shell with explicit header,
children, and footer, plus an optional explicit stylesheet URL. ui/form.Field renders associated labels, bound values,
required/maxlength, password/text/textarea variants, and error-only aria-invalid
and aria-describedby relationships. ui/form.CSRF requires an explicit token
from session middleware. Components do not create tokens, validate requests, or
bypass CSRF. Binding/validation stays in forms and the application.
The starter, articles list/detail/login/edit, and catalog/admin pages use these compiled components. Existing authorization, ownership filtering, CSRF, and persistence paths remain; real PostgreSQL HTTP regression tests verify them. FORM-01 is still Partial: richer typed binding, validators, widgets and ModelForms are separate work. Administration’s broader battery is also not completed here.
Application UI uses compiled .templ components. Follow the
component authoring guide. Template strings, global definitions and
template.HTML are not component content.
htmx and REST ¶
Use the bundled htmx 4 integration for component HTML requests,
or keep REST/JSON handlers for API clients. The standard page layout accepts
a Scripts slot; pass htmx.Script() and mount htmx.Handler() to load the
embedded runtime. No CDN, npm, or deployment-time asset collection is required.