Skip to content

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.

gsx
package views

component Tag(label string, active bool) {
	<span class={ "tag", "tag--active": active }>
		{ label }
	</span>
}

Renders:

html
<span class="tag tag--active">stable</span>

▶ Open in Playground

Expressions can contribute classes too:

gsx
<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.

gsx
<div style={
	"display: block",
	"color: " + accent,
	"opacity: 0": hidden,
}>...</div>

Use a CSS literal when a dynamic declaration is clearer as CSS text:

gsx
<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.

gsx
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:

html
<div class="card featured" style="color: blue; margin: 0"></div>

▶ Open in Playground

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:

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 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:

toml
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:

gsx
<button class={ "btn", if open { "btn-open" } else { "btn-closed" } }>...</button>

Use switch for several choices:

gsx
<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:

gsx
<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 @{...}.

gsx
package views

component Card(w int, userColor string) {
	<style>
		.card {
			width: @{ w }px;
			color: @{ userColor };
		}
	</style>
}

Renders:

html
<style>.card{width: 12px;color: teal}</style>

▶ Open in Playground

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.