Skip to content

Attributes

Use quoted strings for static values, braces for Go expressions, and typed literals when an attribute contains interpolated text, JavaScript, or CSS.

gsx
<input name="email" value={value} required={required}/>
<a href=f`/users/@{id}`>Profile</a>
<button @click=js`save(@{id})`>Save</button>
<div style=css`color:@{color}`>...</div>

Expression attributes

Use name={expr} to bind a Go value.

gsx
package views

component Link(url string, label string, count int) {
	<a href={url} data-count={count}>{ label }</a>
}

Renders:

html
<a href="/p?q=a&amp;b" data-count="42">Docs</a>

▶ Open in Playground

data-count={count} formats a numeric value as attribute text. Quoted values are literal: title="Item @{id}" does not scan for @{} holes.

Boolean attributes

A bool renders as presencetrue writes a bare attribute, false omits it — on every name except the handful whose HTML values are the strings "true" and "false".

gsx
<div data-open={open} hidden={done} aria-expanded={open}>
html
<div data-open hidden aria-expanded="true">

The exceptions are the aria-* states plus contenteditable, spellcheck, draggable, writingsuggestions, SVG focusable and MathML displaystyle. There the string carries meaning absence cannot: a screen reader announces aria-expanded="false" as collapsed and says nothing at all when the attribute is missing, and contenteditable, spellcheck and displaystyle inherit, so only ="false" opts a subtree out.

Everything else is presence — including title={b} and library names like x-show/hx-boost, which you write as strings or js literals anyway (x-show=js`open` , hx-boost="false"). Static and string values are never touched by this rule: required="foo" and hx-boost="false" render verbatim. style={b} is its own case: it goes through the class/style merge, which always writes a value.

A bare attribute (<input required>) is always present, wherever it travels — including through a component's attrs bag.

  • Want the string from a Go bool? Say so: data-x={ strconv.FormatBool(b) }.
  • Want presence on an aria-* name? gsx.Toggle(b).

Attributes where absence is the permissive state

sandbox={untrusted} and crossorigin={cors} omit the attribute when the bool is false — and for sandbox, no attribute means no sandbox at all. Make sure absence is the state you want, or write the value: sandbox="allow-scripts".

gsx
package views

component Field(on bool) {
	<input type="text" class="form-control" required disabled={on}/>
}

Renders:

html
<input type="text" class="form-control" required disabled>

▶ Open in Playground

Conditional attributes

Use { if cond { ... } } inside an opening tag when a condition contributes one or more attributes.

gsx
package views

component Badge(featured bool) {
	<span
		{ if featured {
			class="featured"
		} }
	>
		content
	</span>
}

Renders:

html
<span class="featured">content</span>

▶ Open in Playground

An else branch works too: { if active { class="active" } else { class="idle" } }.

{ switch … } is the switch counterpart, for when one of several mutually exclusive attribute sets applies:

gsx
<div
    { switch status {
    case "ok":
        data-state="ok"
    case "warn", "error":
        data-state="bad" aria-live="polite"
    default:
        data-state="unknown"
    } }
>

It takes the same shapes Go's switch does — a tag expression, multi-value case lists, a tagless switch { case cond: … }, and default — and evaluates its tag exactly once. fallthrough is not supported: an attribute list is not a statement list, so falling through would emit two arms' attributes and duplicate names. List the shared attributes in each case instead.

An arm with no matching case contributes nothing, exactly like an if with no else.

Spread { x… } — ordered

Spread a gsx.Attrs bag with { bag... }; entries render in slice order.

gsx
package views

import "github.com/gsxhq/gsx"

component Card(extra gsx.Attrs) {
	<div { extra... }>content</div>
}

Renders:

html
<div id="box" data-active="true">content</div>

▶ Open in Playground

  • Source order is kept.
  • A bool takes the same rule as an element attribute: presence, except on the names whose HTML values are "true"/"false" (aria-*, contenteditable, spellcheck, draggable). gsx.Toggle(b) forces presence on any name.
  • Spread values are escaped for the destination attribute; URL and srcset destinations also run scheme sanitization.
  • gsx.AttrMap.ToAttrs() sorts map keys.

Treat keys as trusted code. Build gsx.Attr and gsx.Attrs keys only from developer-controlled strings; never use user input as a key. Values are still escaped or URL-sanitized for their destination. For dynamic JavaScript or CSS, use a typed literal on the final native element; see JavaScript and CSS contexts.

Ordered-attrs literal {{ "k": v }}

Use {{ "k": v }} as the value of a component's attrs-bag parameter when the call site must declare the bag in source order.

gsx
package views

import "github.com/gsxhq/gsx"

component Counter(signals gsx.Attrs, children gsx.Node) {
	<button { signals... }>{ children }</button>
}

component Page() {
	<Counter signals={{ "data-signals": "{count:0}", "data-text": "$count", "data-on-click": "$count++" }}>Count</Counter>
}

Renders:

html
<button data-signals="{count:0}" data-text="$count" data-on-click="$count++">Count</button>

▶ Open in Playground

  • The literal is valid only as a component attribute value.
  • Keys must be quoted strings.
  • A bool takes the same rule as an element attribute: presence, except on the names whose HTML values are "true"/"false" (aria-*, contenteditable, spellcheck, draggable). gsx.Toggle(b) forces presence on any name.
  • The last scalar value for a key wins.
  • All class and style values compose.

Contributing to the declared attrs input

gsx
<Panel id="profile" attrs={computed} { defaults... } attrs={{ "role": "region" }}/>
  • Ordinary unmatched attributes, conditional attributes, spreads, attrs={expr}, and attrs={{...}} compose in authored order.
  • Names inside a conditional-attribute group are bag contributors; they do not conditionally fill ordinary component parameters.
  • attrs={expr} accepts a computed attrs-bag value.
  • Repeated explicit attrs contributors are allowed; they do not fill an ordinary parameter slot.
  • The component must declare the reserved attrs parameter.

Contextual escaping

Ordinary values are attribute-escaped. Dynamic URL expressions, interpolated URL literals, and forwarded URL values also reject dangerous schemes. A quoted URL authored directly on a native element is trusted author text and is emitted without scheme validation.

gsx
package views

component Link(href string, label string) {
	<a href={href}>{ label }</a>
}

Renders:

html
<a href="about:invalid#gsx">click me</a>

▶ Open in Playground

Use typed literals for JavaScript- and CSS-valued attributes:

gsx
<button @click=js`save(@{id})`>Save</button>
<div style=css`color:@{color}`>...</div>

See Escaping and JavaScript for the complete context rules.

Interpolating attribute literals

Use f literals to combine static text with typed @{expr} holes.

gsx
package views

component Row(id string, n int) {
	<div data-key=f`row-@{id}-@{n}` title=f`Item @{id |> upper}`>Row</div>
}

Renders:

html
<div data-key="row-a&amp;b-5" title="Item A&amp;B">Row</div>

▶ Open in Playground

  • Use f"..." when the content contains a backtick.
  • Inside a backtick-delimited literal, \` writes a literal backtick.
  • \@{ writes a literal @{ instead of opening a hole.

On a component tag

gsx
<PageHeader title="Tickets" subtitle=f`@{count} tickets`/>

A matching string parameter receives the assembled string. A matching gsx.Node parameter receives an escaped text node. An unmatched name falls through to the component's attrs bag.

URL attributes sanitize the whole value

Static text and holes in an interpolated URL literal are assembled before the URL scheme check.

gsx
package views

component Link(x string) {
	<a href=f`javascript:@{x}`>go</a>
}

Renders:

html
<a href="about:invalid#gsx">go</a>

▶ Open in Playground

data:image literals

gsx
<img src=f`data:image/png;base64,@{b64}`/>
<img src={imageBytes |> dataURL("image/png")}/>
<img src={"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg'%3E%3C/svg%3E"}/>

Image sinks accept both base64 and strictly validated plain-text payloads. A data: literal is rejected on strict navigation sinks such as href, and a constant value that its sink always blocks warns at generate time. See Escaping for image and navigation sink rules.

class and style are merge targets

Interpolated class and style values merge with a forwarded attrs bag.

gsx
package views

import "github.com/gsxhq/gsx"

component Badge(variant string, attrs gsx.Attrs) {
	<span class=f`badge-@{variant}` { attrs... }>Hi</span>
}

Renders:

html
<span class="badge-x hl" id="a">Hi</span>

▶ Open in Playground

See Styling for precedence.