Styling
Use composable class and style values when an element has a fixed base plus optional or caller-supplied parts.
Compose classes
Write a class={...} list with always-on strings and "name": condition entries. Included parts keep source order.
package views
component Tag(label string, active bool) {
<span class={ "tag", "tag--active": active }>
{ label }
</span>
}Renders:
<span class="tag tag--active">stable</span>Expressions can contribute classes too:
<button class={ "btn", sizeClass, "btn-disabled": disabled }>Save</button>Compose inline styles
Each entry in a style={...} list is a complete CSS declaration. A condition after : includes that declaration only when it is true.
<div style={
"display: block",
"color: " + accent,
"opacity: 0": hidden,
}>...</div>Use a CSS literal when a dynamic declaration is clearer as CSS text:
<div style={ "display: none": hidden, css`width:@{width}px` }>...</div>Dynamic CSS values are filtered for their CSS context. See Escaping for the safety rules and trusted-value boundary.
Merge forwarded class and style
Scalar attributes from a spread keep the spread's source position. One case is different: a direct local class or style on the component root merges before the forwarded { attrs... } fallthrough bag, even when that spread appears first. Conditional attribute blocks and multiple explicit spreads remain source ordered. The default class merger keeps the last occurrence of an exact duplicate token. For style, the later value for the same property wins.
For example, <div { attrs... } class="card"> still puts card before the caller's class tokens.
package views
import "github.com/gsxhq/gsx"
component Card(children gsx.Node, attrs gsx.Attrs) {
<div class="card" style="color: red" { attrs... }>{ children }</div>
}Renders:
<div class="card featured" style="color: blue; margin: 0"></div>The example keeps both class tokens, replaces color: red with the caller's color: blue, and adds the caller's margin. See Attributes for general spread ordering and precedence.
An interpolated class literal is another mergeable class value:
package views
import "github.com/gsxhq/gsx"
component Badge(variant string, attrs gsx.Attrs) {
<span class=f`badge-@{variant}` { attrs... }>Hi</span>
}Renders:
<span class="badge-x hl" id="a">Hi</span>See Attributes for f literal syntax.
Tailwind-aware class merging
Exact-token deduplication does not resolve conflicting Tailwind utilities such as px-4 px-8. Configure a Tailwind-aware merger when your project needs those semantics:
class_merger = "myapp/twcfg.Merge"The configured symbol has the signature func([]string) string. See Configuration for the contract and a tailwind-merge-go wrapper.
Choose one value with if or switch
Conditional entries are additive. Use a value-form if when one of two values should contribute:
<button class={ "btn", if open { "btn-open" } else { "btn-closed" } }>...</button>Use switch for several choices:
<span class={
"badge",
switch tone {
case "success": "badge-success"
case "warning": "badge-warning"
default: "badge-neutral"
},
}>...</span>The selected arm contributes one string. With no matching arm and no else or default, it contributes nothing. The same forms work in style={...} lists, where each arm returns a complete declaration.
An arm may also hold a contextual literal — f in class={...}, css in style={...} — the same literal each attribute accepts as a plain entry:
<div style={
switch form {
case ratioPair: css`aspect-ratio: @{w} / @{h}`
default: css`aspect-ratio: @{ratio}`
},
}>...</div>This is how you get punctuation the CSS value filter rejects — here the / separator — into a declaration without reaching for gsx.RawCSS: the separator is static template text and only @{w}/@{h} are holes, so each is still filtered.
Each attribute takes only its own literal language, in every position — as the whole value, as one entry in a list, or as a value-form arm. class takes f; style takes css. Anything else is a compile error.
That is a safety rule, not a stylistic one: the literal's language selects the sanitizer applied to its @{...} holes. An f literal in style would escape its holes for HTML only and never run the CSS value filter, letting a hole inject background:url(javascript:...) into the style attribute; a css literal in class would run the CSS filter over a class string, collapsing an ordinary bg-primary/20 to ZgotmplZ. Attributes that compose nothing (data-*, onclick, …) are unrestricted.
<style> blocks
Use a <style> block for component CSS. Interpolate Go values with @{...}.
package views
component Card(w int, userColor string) {
<style>
.card {
width: @{ w }px;
color: @{ userColor };
}
</style>
}Renders:
<style>.card{width: 12px;color: teal}</style>Interpolated values are CSS-filtered. See Escaping before passing trusted CSS. A css literal can also be stored as a gsx.RawCSS Go value; see Contextual literals as Go values.