Composition
Components compose with JSX-like tags while keeping Go values and package boundaries.
Calling components
Call a component with its name and pass declared inputs as attributes. Use a self-closing tag when it has no nested content.
package views
component Card(title string, featured bool, count int) {
<div class={ "card", "card-featured": featured }>
<h2>{ title }</h2>
<span>{ count }</span>
</div>
}
component Page(t string, n int) {
<Card title={t} featured count={n}/>
}Renders:
<div class="card card-featured"><h2>Hi</h2><span>3</span></div>Here Page passes strings, numbers, and the bare boolean input featured to Card. Package-qualified calls use the same form: <ui.Button label="Save"/>. Names match the callee's authored parameters exactly.
Generic components
Declare Go type parameters after the component name. gsx infers type arguments from the inputs supplied at each call site.
package views
component Badge[T string | int](value T) {
<span class="badge">{ value }</span>
}
component Page() {
<Badge value={"new"}/>
<Badge value={42}/>
}Renders:
<span class="badge">new</span><span class="badge">42</span>Both Badge calls infer T from value, so string and integer instances can appear together in the same component body.
Directly interpolating T requires a renderable constraint; with T any, convert or format it to a supported type, such as { value |> printf("%v") }.
Explicit type arguments
Use Go-style brackets when inference is ambiguous or when the call should state the intended type directly.
package views
component Price[T int | float64](amount T, currency string) {
<b>{ currency }{ amount }</b>
}
component Page() {
<Price amount={9.99} currency="$"/>
<Price amount={42} currency="€"/>
<Price[float64] amount={4} currency="£"/>
}Renders:
<b>$9.99</b><b>€42</b><b>£4</b><Price[float64] amount={4} currency="£"/> keeps the amount in the float64 case even though an untyped 4 would otherwise infer int.
Children {children}
Declare children gsx.Node, then write {children} where the component should render the content between its opening and closing tags.
package views
import "github.com/gsxhq/gsx"
component Card(title string, children gsx.Node) {
<article class="card">
<h3>{ title }</h3>
<div class="card__body">{ children }</div>
</article>
}
component Page() {
<Card title="Hello"><em>composed</em></Card>
}Renders:
<article class="card"><h3>Hello</h3><div class="card__body"><em>composed</em></div></article>Card owns the surrounding markup and chooses the exact placement of the caller's <em>composed</em> node. Use children ...gsx.Node when the component needs each static top-level child separately; inside the body it is a []gsx.Node.
The body is the only fill source for lowercase children; children={...} is rejected. A component without this parameter rejects a non-empty body. See Reserved component inputs.
Named slots
Use ordinary gsx.Node parameters for additional content positions such as a header or footer. Pass markup inline in the matching attribute.
package views
import "github.com/gsxhq/gsx"
component Panel(header gsx.Node, footer gsx.Node) {
<div class="panel">
<header>{ header }</header>
<footer>{ footer }</footer>
</div>
}
component Page() {
<Panel header={ <h1>H</h1> } footer="F"/>
}Renders:
<div class="panel"><header><h1>H</h1></header><footer>F</footer></div>Named slots and children can be used together: named slots receive explicit attributes, while {children} receives the content inside the tag.
Cross-file and cross-package calls
Components in different .gsx files of the same package call each other by name. Imported components use their Go package qualifier.
components.gsx
package views
import "github.com/gsxhq/gsx"
component Button(label string) {
<button class="btn">{ label }</button>
}
component Card(title string, children gsx.Node) {
<section class="card">
<h2>{ title }</h2>
{ children }
</section>
}page.gsx
package views
type HomePage struct {
Title string
}
component (p HomePage) Render() {
<main>
<Card title={p.Title}>
<Button label="Save"/>
</Card>
</main>
}Renders:
<main><section class="card"><h2>Dashboard</h2><button class="btn">Save</button></section></main>Normal Go visibility applies: unexported components stay within their package, and exported components can be called as <ui.Button .../> after importing ui.
Explicit attribute forwarding
Undeclared component attributes are accepted only when the component declares an attrs parameter. Put { attrs... } on the element that should receive them.
package views
import "github.com/gsxhq/gsx"
component Button(variant string, children gsx.Node, attrs gsx.Attrs) {
<button class="btn" data-variant={variant} { attrs... }>{ children }</button>
}
component Page() {
<Button variant="primary" class="w-full" data-test="x" hx-post="/go">Save</Button>
}Renders:
<button class="btn w-full" data-variant="primary" data-test="x" hx-post="/go">Save</button>The component can forward the bag, split selected values across elements, or omit it entirely. Every unmatched input and explicit attrs contributor enters the bag at its authored position. See Attributes — Spread for bag syntax and Escaping for the trust rules applied at the destination.
For JavaScript or CSS attributes with dynamic holes, accept ordinary parameters and build the contextual literal on the final native element; see JavaScript. A component that intentionally accepts trusted code may instead declare a gsx.RawJS or gsx.RawCSS parameter and receive a braced contextual value.
Precedence
The spread's source position controls scalar attributes:
<button type="button" { attrs... } disabled class="button">Save</button>- Before the spread,
typeis a default that the bag can override. - After the spread,
disabledis forced by the component. classandstylecompose instead of replacing one another.
Derived bags
The spread can use any expression that produces gsx.Attrs:
<input { attrs.Without("type")... }/>
<div { attrs.Merge(extra)... }>...</div>
<span { p.Attrs.Without("id")... }>Label</span>Forwarding through components
A component can pass its fallthrough bag into another component call. Each component in the chain still chooses where the attributes finally land.
package views
import "github.com/gsxhq/gsx"
component Icon(name string, attrs gsx.Attrs) {
<span class="icon" data-name={name} { attrs... }>i</span>
}
component SearchIcon(attrs gsx.Attrs) {
<Icon name="search" class="w-5 h-5" { attrs... }/>
}
component Page() {
<SearchIcon class="text-red" aria-label="Search"/>
}Renders:
<span class="icon w-5 h-5 text-red" data-name="search" aria-label="Search">i</span>SearchIcon adds its default class, then forwards the outer caller's class and ARIA label through Icon to the final <span>. The target must declare its own attrs parameter; a composite value parameter does not implicitly accept a bag.
Method components
Declare a component as a method when several views share state held by a named receiver. Authored parameters remain the method's exact Go signature and the per-call markup data.
package views
type UsersPage struct {
Title string
Sort string
}
component (p UsersPage) Page() {
<div>
<p.Grid sort={p.Sort}/>
</div>
}
component (p UsersPage) Grid(sort string) {
<span>{ sort }-{ p.Title }</span>
}Renders:
<div><span>name-Team</span></div>Call another method component through the receiver, as in <p.Grid sort={p.Sort}/>. This keeps page-level state on p without threading it through every component parameter.