Templates
Introduction
A .vuka.html shape file holds its Go in a <script lang="go"> element, followed by the markup that Go renders. You write your components and pages this way, so the data a view renders and the markup that renders it live in one file.
The script has the package, imports, exactly one rendering struct, decorated hooks and methods. Everything after </script> is markup, except the component's <style> and <script lang="ts"> blocks at the end. Logic without a view belongs in .go or .vuka files.
Errors point to the shape file's own line, not the generated Go.
Writing a Component
Save this as components/sales.vuka.html:
<script lang="go">
package components
import "github.com/vuka-lang/ui/v2"
type Sales struct {
Title string
Week []int
Children ui.Node
}
func (s Sales) Total() int {
n := 0
for _, amount := range s.Week { n += amount }
return n
}
</script>
<section class="sales">
<h2>{Title}</h2>
for _, amount := range Week {
<span class="day" class:big={amount > 10}>{amount}</span>
}
<p>Total: {Total()}</p>
{Children}
</section>The struct is the component and its fields are props. Use its tag from another shape, or its generated node value from Go logic.
The same markup participates in static rendering and live render-tree diffing.
Scope and the Receiver
Fields and methods are available by bare name: {Title}, {Total()}. A field or method wins over a package name in a value expression; locals and loop variables shadow fields. A tag such as <Icon/> resolves the package's component type. The receiver named by the script's methods is available too, so {s.Title} makes the choice explicit.
For a field whose name is also a type (Glyph Glyph), {Glyph.Name} and f(Glyph) read the field, while Glyph(x), Glyph{Name: "x"}, []Glyph and new(Glyph) name the type. W0101 flags an ambiguous bare name; use the receiver to disambiguate a value.
The generated receiver is a pointer for a live shape, browser-state fields or pointer-receiver methods, otherwise a value.
The Struct in the File
Exported fields are props, unexported fields are tracked server state or provided dependencies, and ui.State[T] fields are browser state.
A shape is live if it embeds ui.Live, declares a decorated hook or wires a server method to an event. A command-only event remains browser-side and does not make the shape live. The compiler supplies the appropriate rendering interface.
This counter is live, because it declares a mount hook and wires Increment to a click:
<script lang="go">
package components
import (
"context"
"github.com/vuka-lang/ui/v2"
)
type Counter struct {
Start int
n int
}
@ui.Mount
func (c *Counter) mount(ctx context.Context) error { c.n = c.Start; return nil }
func (c *Counter) Increment() { c.n++ }
</script>
<button onClick={Increment}>{n}</button>No struct is E0146, multiple structs E0147, and a script without markup E0148.
A form DTO, record type or other non-render data structure goes in an adjacent logic file. Each reusable rendering partial becomes its own shape file.
The struct's methods may also live in a .vuka file beside the shape: its tracked fields read and write there as in the script. A method in a .go file can't use them (E0155); see where its methods live.
Laying Out the File
The <script lang="go"> comes first: only blank lines and <!-- comments --> may precede it, and a file has one (E0157). Its content is unindented Go. The package clause matches the directory (external test shapes may use the orders_test package). Single and grouped imports precede the one struct, hooks and ordinary methods.
The compiler reads the script as Go, so a </script> inside a string or a comment doesn't end it; a script with no </script> is E0156. Any other <script> — <script lang="ts">, <script is:inline>, a plain one — is markup.
The struct name need not match the filename. A routed page's filename supplies its URL; it is not inferred from the struct name. Package-level declarations in logic files are available in markup; keep the imports for a complete example in its own script. A missing or mismatched package is E0142.
vuka fmt and editor format-on-save format the whole file: the Go as gofmt does, the markup indented two spaces a level, and the CSS of its <style>. See formatting shape files. Document an exported component on its struct. One file renders one shape; there is no second template body after a later function header.
Converting a Header-Form File
Before 2.3, a shape wrote its Go bare above the markup, which started at the first line beginning with <:
<!-- card.vuka.html, before 2.3 -->
package components
type Card struct{ Title string }
<h2>{Title}</h2>That form still compiles in 2.x and is removed in 3.0. vukavet and the editor report it as W0107, with a quick fix. Rewrite every file under a directory, and format it, with:
vuka fmt -rewrite -w .A package may mix both forms while you migrate. See converting shapes for a rewrite that leaves the formatting as it is.
Writing Holes
Holes accept Go expressions; their required type depends on their place:
<td>{o.Ref}</td> | a child |
title={o.Ref}, title="{o.Ref}" | an attribute's value |
href="/orders/{o.ID}" | part of a value |
class:late={o.Late} | a toggled class: a bool or a ui.State[bool] |
<input {attrs...}> | an attribute set |
onClick={c.Inc}, onClick={c.Add(2)} | an event: a method value, or a call with no result, which runs on the event |
bind:value={Q} | two-way: the field's value, and what is typed sets it — a field marked @ui.Bind on a live page (@ui.Bind on the field, its debounce and validate: rules), else an error |
style:--accent={Color} | a CSS variable (any property) beside the element's style, sanitized as a ui.Style |
{{ is a literal brace and {/* … */} a comment.
Go Statements in Markup
A Go statement at the start of a line, with markup for its body, controls what renders. The following is a fragment inside a shape's markup root:
<section>
if len(orders) == 0 {
<p>No orders yet.</p>
} else {
<ul>
for _, order := range orders {
<li key={order.ID}>{order.Ref}</li>
}
</ul>
}
n := len(orders)
switch n {
case 1:
<p>one order</p>
default:
<p>{n} orders</p>
}
</section>The statements are if, else if, else, for (over a range or a condition), switch with case and default, x := expr and x = expr. Text that starts with a keyword is written as a string, {"if you can"}.
They run in the markup's order, as Go's: a hole shows the value at its place, not the value after the statements below it:
<section>
x := 1
<p>{x}</p> <!-- 1 -->
x = 2
<p>{x}</p> <!-- 2 -->
</section>Components and Slots
Using Component Tags
A capitalized tag names a component in the current package; a dotted tag, <components.Pill> or <ui.Card>, names one from an import. Lowercase HTML, SVG and MathML names remain elements, including custom-element names with a hyphen. Component names and attributes are case-sensitive; an unknown tag is E0143, with the nearest matching name when possible.
A generic component takes its type arguments in the tag, <components.List[Task]>, or infers them from its attributes. Close it as </components.List> or </components.List[Task]>.
Setting Props
A struct component's attributes set exported fields. CamelCase mapping turns noPad into NoPad and onSelect into OnSelect. A bare bool attribute is true; quoted text is a string and {expression} is typed. Quoted text setting a ui.Node field is that text.
HTML elements retain HTML names (class, for, data-*, aria-*). Unknown component attributes are errors rather than arbitrary DOM attributes. Function props let a parent supply a callback. A typed node helper in a logic file can still be called from a hole; reusable application markup belongs in its own shape file.
Go visibility applies: an unexported component is local to its package; an exported one can be used through an import. Keep stable key values on repeated live component tags to preserve logical instances across reorderings.
Filling Children and Slots
A slot is a field a caller fills with markup. Content between a component's tags fills its Children ui.Node field. A <slot:actions> directly inside the tag fills its Actions ui.Node field. A slot anywhere else in the caller's markup is an error.
In components/admin.vuka.html, declare both:
<script lang="go">
package components
import "github.com/vuka-lang/ui/v2"
type Admin struct {
Title string
Actions ui.Node
Children ui.Node
}
</script>
<div class="admin">
<header><h1>{Title}</h1>{Actions}</header>
<main>{Children}</main>
</div>The caller, pages/orders.vuka.html, fills the slot and the children:
<script lang="go">
package pages
import "example.com/app/components"
type Orders struct { Count int }
</script>
<components.Admin title="Orders">
<slot:actions><button>New</button></slot:actions>
<p>{Count} orders</p>
</components.Admin>A slot the component doesn't have is an error naming the nearest slot it does have: <components.Admin> has no slot actoins; did you mean <slot:actions>?.
Passing Values to a Slot
A slot field of a func type returning ui.Node is a scoped slot: the component calls it with values, and the caller's markup for it uses them. This list renders each task through its caller's Row:
<script lang="go">
package components
import "github.com/vuka-lang/ui/v2"
type List[T any] struct {
Items []T
Row func(T, int) ui.Node
}
</script>
<ul>
for i, item := range Items {
<li>{Row(item, i)}</li>
}
</ul>The caller names the values with let, in the order the function takes them, and its markup reads them:
<script lang="go">
package pages
import "example.com/app/components"
type Tasks struct { Open []components.Task }
</script>
<components.List items={Open}>
<slot:row let={t, i}>
<b>{i + 1}.</b> {t.Title}
</slot:row>
</components.List>The slot compiles to func(t components.Task, i int) ui.Node, its types taken from the field: List infers T from items, or the tag gives it, <components.List[components.Task]>. A let may name fewer values than the function takes (let={t}), not more.
NOTE
let goes only on a slot whose field is a func. On a ui.Node slot it is an error, as is a let naming more values than the function passes.
Repeating a Slot
A slot field that is a slice of structs takes one element per slot tag, in the order the tags are written. Name the tag in the singular: each <slot:column> adds a Column to Columns. The tag's attributes set the struct's fields, and its content fills the struct's one func field returning ui.Node, else its one ui.Node field.
The kit's ui.Table and ui.DataTable declare their columns this way: Columns []ui.Column[T], where a ui.Column[T] has a Header ui.Node, a Cell func(T) ui.Node and the strings Align, Class and Sort. A table writes each column as a slot, its row named by let:
<script lang="go">
package pages
import (
"example.com/app/models"
"github.com/vuka-lang/ui/v2"
)
type Books struct { Shelf []models.Book }
</script>
<ui.Table rows={Shelf}>
<slot:column header="Title" let={b}>
<b>{b.Title}</b>
</slot:column>
<slot:column align="right" let={b}>
<slot:header><abbr title="Pages">pp.</abbr></slot:header>
{b.Pages}
</slot:column>
</ui.Table>A <slot:header> inside a <slot:column> fills that element's Header with markup instead of text.
Giving a Slot Fallback Content
Inside a component's own markup, the slot's tag stands for the slot, and its content is what renders when the caller leaves the slot out:
<script lang="go">
package components
import "github.com/vuka-lang/ui/v2"
type Dialog struct {
Title string
Actions ui.Node
Children ui.Node
}
</script>
<section>
<h2>{Title}</h2>
<slot:children><p>Nothing to show.</p></slot:children>
<menu>
<slot:actions><button>Close</button></slot:actions>
</menu>
</section><slot:actions /> with no content is {Actions}. A slot left out, or filled with nothing but white space, shows the fallback.
Checking Whether a Slot Is Filled
ui.Has(Actions) reports whether the caller filled a slot: false for a slot left out and for one holding only white space. Use it to leave out the markup around a slot:
<section>
{Children}
if ui.Has(Footer) {
<footer>{Footer}</footer>
}
</section>Requiring a Slot
Tag a slot field vuka:"required" and every tag must fill it. A tag that doesn't is E0158:
<script lang="go">
package components
import "github.com/vuka-lang/ui/v2"
type Card struct {
Header ui.Node `vuka:"required"`
Children ui.Node `vuka:"required"`
Footer ui.Node
}
</script>
<article>
<header>{Header}</header>
{Children}
<slot:footer />
</article>The tag works on any prop; a required Children needs content between the tags.
Forwarding a Slot
A component passes a slot it was given to another as an attribute, or as that component's slot holding just the field:
<components.Dialog title="Edit" actions={Actions} />
<components.Dialog title="Edit">
<slot:actions>{Actions}</slot:actions>
</components.Dialog>Both pass the same node: a slot holding one ui.Node adds no wrapper around it.
Where Slot Content Runs
Slot content belongs to the shape that writes it, though the component renders it. Its holes read the writer's fields, its onClick={Remove(t.ID)} calls the writer's method, and its browser state is the writer's: a live page filling a live child's slot gets its own events and state back, and renders again after its event, while the child keeps its own state.
Island Tags
An imported frontend component is an island tag, with props read from its file:
<script lang="go">
package pages
import Clock "../frontend/clock.tsx"
type WorldClock struct { Zone string }
</script>
<main><Clock zone={Zone} client:visible /></main>See Islands for supported frameworks, prop inference, optional values, fallback and hydration strategies.
Scripts and Styles
A <script lang="ts"> after a template's markup is its component's script, and a <style scoped> its CSS — one of each per shape. This chart shape has both:
<script lang="go">
package components
import "example.com/app/models"
type Chart struct {
Title string
Points []models.Point
}
</script>
<figure class="chart">
<figcaption>{Title}</figcaption>
</figure>
<script lang="ts">
import { component } from 'vuka:component'
export default component<Chart>((el, props, ctx) => ({
update(next) { el.dataset.n = String(next.Points.length) },
destroy() {},
}))
</script>
<style scoped>
figcaption { font-weight: bold }
</style>The Component Script
The script runs once per instance: el is its root, props the struct's data fields as JSON (ui.Node, func and unexported fields are not sent), update runs when a live re-render changes them, destroy when the instance leaves the page.
ctx.live calls the component's exported methods (ctx.live.reorder(ids) is Reorder), ctx.state("open") is a ui.State field's handle — get(), set(v), watch(fn) — and ctx.navigate(href) / ctx.patch(href) move the page.
A live page's script can use page<Orders>((el, live) => …): live.q is the field Q's handle (get(), set(v) for a field marked @ui.Bind), live.reorder a method.
Script Types
The types come from the Go struct: component<Chart> knows props.Title, ctx.live's methods and ctx.state's names. The vuka command writes them to .vuka/vuka-component.d.ts, and vuka lsp gives each script to a TypeScript language server with them — completion, hover, definition and errors in the .vuka.html — when typescript-language-server is on PATH or in node_modules/.bin ($VUKA_TSLS names one).
Bundling and Scoping
The vuka command bundles the script and scopes the style to the template's own elements (:global(…) escapes). A style has no holes.
Inline Scripts
<script is:inline> is written as it is, not bundled; its one hole is {json(x)}, the value as script-safe JSON:
<script is:inline>window.points = {json(Points)};</script>Building Shapes
vuka run, vuka build and vuka test compile .vuka.html through an overlay without writing among your sources. vuka gen writes each shape's generated Go (x_html.go) for ordinary Go builds.
A package may mix logic .go and .vuka files with shape files in the same scope. Errors and editor navigation map back to source. The generator also emits page routes, typed links, scoped CSS and component-script types when the project uses them.
WARNING
The emitted render methods and node-construction calls are compiler output. Keep the shape file as the rendering source instead of editing generated Go.
Reading the Render Context
A template can read ctx, the context.Context of its current render, without storing it on the page or passing it through every component. This section shows its markup only to visitors who may view analytics:
<script lang="go">
package dashboard
import "github.com/vuka-lang/web/v2/auth"
type Analytics struct{}
</script>
<section>
me := auth.Current(ctx)
if me != nil && me.Can("view_analytics") {
<p>Analytics</p>
}
</section>This works throughout a shape's markup. Existing explicit context parameters in Go methods keep their meaning. Context-sensitive markup runs at render time with the HTTP request or live session's current context. The context is not a component prop and is not sent to the browser.
Keeping Rendering Cheap and Safe
Keep rendering free of writes and expensive database work: fetch data in the mount hook, loaders or handlers. Context-sensitive live output is rendered again even when its props and tracked fields have not changed. This avoids reusing output after a context-dependent permission or locale changes; it does not refresh an application's authentication store by itself.
WARNING
Templates are trusted Go application code, not a sandbox for user-supplied markup. Never print tokens or other secret context values, put context in browser props, or share personalized output through a global cache.
Permission checks in a template control presentation only: guards and handlers must authorize every protected action on the server. Live applications must also refresh or invalidate identities using their auth layer when permissions change. Preserve request cancellation and deadlines; do not save the render context in globals or long-lived component fields.