GSX Syntax Reference
Overview
.gsx files are Go source files extended with a template syntax for declaring UIs. The tui generate command reads .gsx files and produces _gsx.go files containing standard Go code. You never edit the generated files by hand.
A .gsx file can contain:
- A
packagedeclaration andimportblock (standard Go) - Type declarations and regular Go functions (
type,func) - Pure template components (
templ Name(params) { ... }) - Struct method components (
templ (s *Struct) Render() { ... }) - Control flow directives (
if,for,:=) - HTML-like elements (
<div>,<span>, etc.)
File structure
Every .gsx file starts with a standard Go package and import block:
package mypackage
import (
"fmt"
tui "github.com/grindlemire/go-tui"
)
The rest of the file can mix Go declarations (types, functions, variables) with templ component definitions in any order.
Component syntax
Pure components
A pure component is a stateless function that takes parameters and returns UI elements. Define one with the templ keyword:
templ Greeting(name string) {
<span class="text-cyan">{"Hello, " + name}</span>
}
Parameters use standard Go function parameter syntax. Any valid Go type works:
templ UserList(users []string, maxVisible int) {
<div class="flex-col">
for i, u := range users {
if i < maxVisible {
<span>{u}</span>
}
}
</div>
}
Pure components cannot host constructs that mount against a receiver: the component elements (<input>, <textarea>, <modal>, <markdown>), @expr component expressions, and @Factory() calls that return a component (a struct component, a tui.Component, or a type whose Render method is written in plain Go). Put those in a struct method component, which supplies the receiver they mount against. tui generate reports an error with a message pointing at the fix if you use one in a pure component.
Children slot
Pure components can accept nested content from their caller using {children...}:
templ Card(title string) {
<div class="border-rounded p-1">
<span class="font-bold">{title}</span>
{children...}
</div>
}
Call it with children:
<Card title="Status">
<span class="text-green">All systems operational</span>
</Card>
{children...} is only available in pure templ components, not in struct method components.
Struct method components
A struct component has its own state and lifecycle. Define the render method with templ using a receiver:
type counter struct {
count *tui.State[int]
}
func Counter() *counter {
return &counter{
count: tui.NewState(0),
}
}
templ (c *counter) Render() {
<div class="flex gap-2">
<span class="font-bold">{fmt.Sprintf("Count: %d", c.count.Get())}</span>
</div>
}
The method name must be Render and it takes no parameters. The receiver can be a pointer or value type, though pointer receivers are standard.
Calling components
Call a pure component like an element, passing parameters as attributes:
<Greeting name="World" />
<Card title="Info">
<span>Some content</span>
</Card>
Component names must start with an uppercase letter to distinguish them from built-in elements.
Regular Go functions
Standard Go functions work normally in .gsx files:
func formatPercent(v, max int) string {
if max == 0 {
return "0%"
}
return fmt.Sprintf("%d%%", v*100/max)
}
These are passed through to the generated file without transformation.
Elements
Container elements
These elements can have children and support flexbox layout attributes.
<div> -- Block container, and the main layout element. Default direction is row.
<div class="flex-col gap-1 p-1 border-rounded">
<span>First</span>
<span>Second</span>
</div>
<ul> -- Unordered list container. Use with <li> children.
<ul class="flex-col">
<li>Item one</li>
<li>Item two</li>
</ul>
<li> -- List item. Renders with a bullet prefix. Should be a child of <ul>.
<table> -- Table container for tabular data. Supports all container attributes.
Text elements
These hold text content and support text styling but not flex container attributes like direction or justify.
<span> -- Inline text container for styled text content.
<span class="font-bold text-cyan">Status: OK</span>
<p> -- Paragraph element for text blocks.
Interactive elements
<button> -- Clickable element. Supports text and event attributes. Attach a ref for click handling:
<button ref={s.myBtn} class="px-2 border-rounded text-green">{" Save "}</button>
<input /> -- Single-line text input. Self-closing. Bind value to a *State[string] for two-way binding. Also accepts placeholder, width, border, focusColor, borderGradient, focusGradient, onSubmit, and onChange.
<input value={s.text} placeholder="Type here..." width={30} border={tui.BorderRounded} />
<textarea /> -- Multi-line text input with word wrapping. Self-closing. Bind value to a *State[string] for two-way binding. Also accepts placeholder, width, maxHeight, border, focusColor, borderGradient, focusGradient, submitKey, and onSubmit.
<textarea value={s.note} placeholder="Write here..." width={40} maxHeight={6} border={tui.BorderRounded} />
<modal> -- Full-screen overlay dialog. Bind open to a *State[bool] for visibility. Accepts children for the dialog content. Supports backdrop, closeOnEscape, closeOnBackdropClick, trapFocus, keyMap, and class for positioning.
<modal open={s.showDialog} class="justify-center items-center">
<div class="border-rounded p-2 flex-col gap-1 w-40">
<span class="font-bold">Title</span>
<button class="focusable border-rounded" onActivate={s.confirm}>OK</button>
</div>
</modal>
Display elements
<markdown /> -- Renders a markdown string into the widget tree. Self-closing. Provide content through source (a string expression) or state (a *State[string] that re-renders on change). Also accepts width and theme. Owns no scroll or keys, so wrap it in a scrollable container for long documents.
<markdown source={s.doc} width={80} />
<progress /> -- Progress bar. Self-closing. Accepts value, max, and width.
<progress value={75} max={100} width={20} />
<hr /> -- Horizontal rule. Self-closing. Accepts only id and class.
<hr />
<br /> -- Line break. Self-closing. Accepts only id and class.
<br />
Self-closing elements
<input />, <textarea />, <markdown />, <progress />, <hr />, and <br /> are self-closing and cannot have children. Writing <input>children</input> produces a compile error.
Attributes
String attributes
Pass string values with double quotes:
<div class="flex-col gap-1" id="header">
<span text="Hello" />
</div>
Integer and float attributes
Pass numbers directly without braces:
<div width={40} height={10} gap={2}>
<div flexGrow={1.5} />
</div>
Boolean attributes
Pass true or false, or use the shorthand (attribute name alone means true):
<div focusable={true} />
<div focusable /> // equivalent
<div disabled={false} />
Go expression attributes
Pass any Go expression inside braces:
<div
width={computeWidth()}
textStyle={tui.NewStyle().Bold().Foreground(tui.ANSIColor(tui.Cyan))}
scrollable={tui.ScrollVertical}
scrollOffset={0, s.scrollY.Get()}
>
Ref attributes
Bind an element to a ref for later access in handlers:
<div ref={s.myRef} class="border-single p-1" />
See the Refs Reference for Ref, RefList, and RefMap details.
Key attributes
Inside for loops, the key attribute gives an element a stable identity among its siblings, like keys in React. It does two things. With a ref bound to a RefMap, it provides the map key:
for _, name := range items {
<div ref={s.itemRefs} key={name}>{name}</div>
}
On component elements (<textarea>, <input>, <modal>, <markdown>), it also identifies the cached component instance, so a component's internal state follows its item when the list reorders or items are inserted:
for _, msg := range s.messages {
<textarea key={msg.ID} placeholder={msg.Author} />
}
Without key, components in a loop are identified by the loop's index or map key, which is fine for stable lists but ties state to position when a slice reorders. The key must be unique among siblings of the innermost loop; in nested loops, outer loops contribute their own identity automatically. The value must be a Go expression: key="literal" is a compile error.
A key on a component element outside any loop changes identity per value: when the expression changes, the old instance is swept and a fresh one mounts. Use this to reset a component's internal state when the thing it represents changes, such as <textarea key={c.activeDraftID} /> clearing between drafts.
Struct component calls (@ChatMessage(item)) have no attributes, so to key one, put the key on a wrapper element. A key on any element applies to every component mounted beneath it:
for _, msg := range s.messages {
<div key={msg.ID} class="flex-col">
@ChatMessage(msg)
</div>
}
Keys compose by depth. A key replaces the loop position at its own level, loops nested inside a keyed wrapper still contribute their own identity, and a key nested under another key appends to it rather than erasing it. One caveat: components passed into a children slot are mounted at the caller's site, so a keyed wrapper inside the receiving component does not re-key them.
Attribute reference
Generic attributes (all elements)
| Attribute | Type | Generated option | Description |
|---|---|---|---|
id |
string | -- | Unique identifier |
class |
string | (varies) | Tailwind-style classes |
disabled |
bool | -- | Disables the element |
ref |
expression | ref.Set(el) |
Binds element to a ref |
deps |
expression | -- | Explicit state dependencies |
key |
expression | -- | Loop item identity: component cache key, RefMap key with ref |
Layout attributes
| Attribute | Type | Generated option |
|---|---|---|
width |
int | tui.WithWidth(n) |
widthPercent |
int | tui.WithWidthPercent(n) |
height |
int | tui.WithHeight(n) |
heightPercent |
int | tui.WithHeightPercent(n) |
minWidth |
int | tui.WithMinWidth(n) |
minHeight |
int | tui.WithMinHeight(n) |
maxWidth |
int | tui.WithMaxWidth(n) |
maxHeight |
int | tui.WithMaxHeight(n) |
Available on: div, ul, li, table, span, p, button, input (width only), progress (width only).
Flex attributes
| Attribute | Type | Generated option |
|---|---|---|
direction |
tui.Direction |
tui.WithDirection(d) |
justify |
tui.Justify |
tui.WithJustify(j) |
align |
tui.Align |
tui.WithAlign(a) |
gap |
int | tui.WithGap(n) |
flexGrow |
float | tui.WithFlexGrow(f) |
flexShrink |
float | tui.WithFlexShrink(f) |
alignSelf |
tui.Align |
tui.WithAlignSelf(a) |
Available on: div, ul, li, table.
Spacing attributes
| Attribute | Type | Generated option |
|---|---|---|
padding |
int | tui.WithPadding(n) |
margin |
int | tui.WithMargin(n) |
Available on: div, ul, li, table, span, p, button.
Visual attributes
| Attribute | Type | Generated option |
|---|---|---|
border |
tui.BorderStyle |
tui.WithBorder(b) |
borderStyle |
tui.Style |
tui.WithBorderStyle(s) |
borderTitle |
string | tui.WithBorderTitle(s) |
background |
tui.Style |
tui.WithBackground(s) |
Available on: div, ul, li, table, span, p, button.
Text attributes
| Attribute | Type | Generated option |
|---|---|---|
text |
string | tui.WithText(s) |
textStyle |
tui.Style |
tui.WithTextStyle(s) |
textAlign |
tui.TextAlign |
tui.WithTextAlign(a) |
Available on: span, p, button.
Event and focus attributes
| Attribute | Type | Generated option |
|---|---|---|
focusable |
bool | tui.WithFocusable(b) |
onFocus |
func(*tui.Element) |
tui.WithOnFocus(fn) |
onBlur |
func(*tui.Element) |
tui.WithOnBlur(fn) |
onActivate |
func() |
tui.WithOnActivate(fn) |
Available on: div, ul, li, table, span, p, button, input.
Modal attributes
| Attribute | Type | Description |
|---|---|---|
open |
*State[bool] |
Controls modal visibility (required) |
backdrop |
string |
"dim" (default), "blank", or "none" |
closeOnEscape |
bool |
Escape closes modal (default true) |
closeOnBackdropClick |
bool |
Backdrop click closes modal (default true) |
trapFocus |
bool |
Restrict Tab to modal children and block unhandled keys from parents (default true) |
keyMap |
expression |
Custom KeyMap bindings for the modal |
Scroll attributes
| Attribute | Type | Generated option |
|---|---|---|
scrollable |
tui.ScrollMode |
tui.WithScrollable(m) |
scrollOffset |
int, int | tui.WithScrollOffset(x, y) |
scrollbarStyle |
tui.Style |
tui.WithScrollbarStyle(s) |
scrollbarThumbStyle |
tui.Style |
tui.WithScrollbarThumbStyle(s) |
hideScrollbar |
bool |
tui.WithScrollbarHidden(b) |
Available on: div, ul, li, table.
Input-specific attributes
| Attribute | Type | Description |
|---|---|---|
value |
*tui.State[string] |
Two-way text binding |
placeholder |
string | Placeholder text when empty |
width |
int | Input width in characters (default 20) |
border |
tui.BorderStyle |
Border style |
textStyle |
tui.Style |
Text styling |
placeholderStyle |
tui.Style |
Placeholder text styling (default: dim) |
cursor |
rune | Cursor character (default '▌') |
focusColor |
tui.Color |
Border color when focused (default Cyan) |
borderGradient |
tui.Gradient |
Border gradient when unfocused |
focusGradient |
tui.Gradient |
Border gradient when focused (overrides focusColor) |
onSubmit |
func(string) |
Called when Enter is pressed |
onChange |
func(string) |
Called when text changes |
Textarea-specific attributes
| Attribute | Type | Description |
|---|---|---|
value |
*tui.State[string] |
Two-way text binding |
placeholder |
string | Placeholder text when empty |
width |
int | Width in characters (default 40) |
maxHeight |
int | Maximum height in rows (0 = unlimited) |
border |
tui.BorderStyle |
Border style |
textStyle |
tui.Style |
Text styling |
placeholderStyle |
tui.Style |
Placeholder text styling (default: dim) |
cursor |
rune | Cursor character (default '▌') |
focusColor |
tui.Color |
Border color when focused (default Cyan) |
borderGradient |
tui.Gradient |
Border gradient when unfocused |
focusGradient |
tui.Gradient |
Border gradient when focused (overrides focusColor) |
submitKey |
tui.Key |
Key that triggers submit (default KeyEnter) |
onSubmit |
func(string) |
Called when submit key is pressed |
Progress-specific attributes
| Attribute | Type | Description |
|---|---|---|
value |
int | Current progress (0 to max) |
max |
int | Maximum progress value |
Go expressions
Text content
Embed Go expressions inside braces to produce text:
<span>{fmt.Sprintf("Count: %d", count)}</span>
<span>{"literal string"}</span>
<span>{s.name.Get()}</span>
Attribute values
Use braces for dynamic attribute values:
<div width={s.computeWidth()} textStyle={s.getActiveStyle()} />
Component calls
Call components with the @ prefix or as XML-like tags:
<Card title={fmt.Sprintf("Item %d", i)}>
<span>{content}</span>
</Card>
Control flow
if / else
Conditionally render elements:
if s.loading.Get() {
<span class="text-yellow">Loading...</span>
} else {
<span class="text-green">Ready</span>
}
Chain conditions:
if count > 10 {
<span class="text-red">High</span>
} else if count > 5 {
<span class="text-yellow">Medium</span>
} else {
<span class="text-green">Low</span>
}
The condition is any valid Go boolean expression.
for
Loop over collections:
for i, item := range s.items.Get() {
<span>{fmt.Sprintf("%d. %s", i+1, item)}</span>
}
Supports all standard Go range patterns:
for _, v := range items { // index ignored
for i := range items { // value ignored
for i, v := range items { // both used
Local bindings (:=)
Bind an element to a local variable for reuse:
badge := <span class="text-cyan font-bold">{fmt.Sprintf("%d", s.count.Get())}</span>
<div class="flex gap-2">
{badge}
</div>
Note the := binding assigns both element expressions (starting with <) to a local variable as well as normal Go expressions.
Tailwind class reference
Classes are set via the class attribute. Multiple classes are space-separated. The compiler maps each class to one or more tui.With* option functions.
Layout
| Class | Generated option |
|---|---|
flex |
tui.WithDirection(tui.Row) |
flex-row |
tui.WithDirection(tui.Row) |
flex-col |
tui.WithDirection(tui.Column) |
gap-N |
tui.WithGap(N) |
Flex sizing
| Class | Generated option |
|---|---|
grow |
tui.WithFlexGrow(1) |
grow-0 |
tui.WithFlexGrow(0) |
shrink |
tui.WithFlexShrink(1) |
shrink-0 |
tui.WithFlexShrink(0) |
flex-1 |
tui.WithFlexGrow(1), tui.WithFlexShrink(1) |
flex-auto |
tui.WithFlexGrow(1), tui.WithFlexShrink(1) |
flex-initial |
tui.WithFlexGrow(0), tui.WithFlexShrink(1) |
flex-none |
tui.WithFlexGrow(0), tui.WithFlexShrink(0) |
flex-grow-N |
tui.WithFlexGrow(N) |
flex-shrink-N |
tui.WithFlexShrink(N) |
Justify content
| Class | Generated option |
|---|---|
justify-start |
tui.WithJustify(tui.JustifyStart) |
justify-center |
tui.WithJustify(tui.JustifyCenter) |
justify-end |
tui.WithJustify(tui.JustifyEnd) |
justify-between |
tui.WithJustify(tui.JustifySpaceBetween) |
justify-around |
tui.WithJustify(tui.JustifySpaceAround) |
justify-evenly |
tui.WithJustify(tui.JustifySpaceEvenly) |
Align items
| Class | Generated option |
|---|---|
items-start |
tui.WithAlign(tui.AlignStart) |
items-center |
tui.WithAlign(tui.AlignCenter) |
items-end |
tui.WithAlign(tui.AlignEnd) |
items-stretch |
tui.WithAlign(tui.AlignStretch) |
Self-alignment
| Class | Generated option |
|---|---|
self-start |
tui.WithAlignSelf(tui.AlignStart) |
self-center |
tui.WithAlignSelf(tui.AlignCenter) |
self-end |
tui.WithAlignSelf(tui.AlignEnd) |
self-stretch |
tui.WithAlignSelf(tui.AlignStretch) |
Text alignment
| Class | Generated option |
|---|---|
text-left |
tui.WithTextAlign(tui.TextAlignLeft) |
text-center |
tui.WithTextAlign(tui.TextAlignCenter) |
text-right |
tui.WithTextAlign(tui.TextAlignRight) |
Width and height
| Class | Generated option |
|---|---|
w-N |
tui.WithWidth(N) |
w-full |
tui.WithWidthPercent(100.00) |
w-auto |
tui.WithWidthAuto() |
w-1/2 |
tui.WithWidthPercent(50.00) |
w-1/3 |
tui.WithWidthPercent(33.33) |
w-2/3 |
tui.WithWidthPercent(66.67) |
h-N |
tui.WithHeight(N) |
h-full |
tui.WithHeightPercent(100.00) |
h-auto |
tui.WithHeightAuto() |
min-w-N |
tui.WithMinWidth(N) |
max-w-N |
tui.WithMaxWidth(N) |
min-h-N |
tui.WithMinHeight(N) |
max-h-N |
tui.WithMaxHeight(N) |
Fraction syntax (w-N/D) computes the percentage at compile time.
Spacing
| Class | Generated option |
|---|---|
p-N |
tui.WithPadding(N) |
px-N |
tui.WithPaddingTRBL(0, N, 0, N) |
py-N |
tui.WithPaddingTRBL(N, 0, N, 0) |
pt-N |
tui.WithPaddingTRBL(N, 0, 0, 0) |
pr-N |
tui.WithPaddingTRBL(0, N, 0, 0) |
pb-N |
tui.WithPaddingTRBL(0, 0, N, 0) |
pl-N |
tui.WithPaddingTRBL(0, 0, 0, N) |
m-N |
tui.WithMargin(N) |
mx-N |
tui.WithMarginTRBL(0, N, 0, N) |
my-N |
tui.WithMarginTRBL(N, 0, N, 0) |
mt-N |
tui.WithMarginTRBL(N, 0, 0, 0) |
mr-N |
tui.WithMarginTRBL(0, N, 0, 0) |
mb-N |
tui.WithMarginTRBL(0, 0, N, 0) |
ml-N |
tui.WithMarginTRBL(0, 0, 0, N) |
Borders
| Class | Generated option |
|---|---|
border |
tui.WithBorder(tui.BorderSingle) |
border-single |
tui.WithBorder(tui.BorderSingle) |
border-double |
tui.WithBorder(tui.BorderDouble) |
border-rounded |
tui.WithBorder(tui.BorderRounded) |
border-thick |
tui.WithBorder(tui.BorderThick) |
Border colors: border-red, border-green, border-blue, border-cyan, border-magenta, border-yellow, border-white, border-black. Each generates tui.WithBorderStyle(tui.NewStyle().Foreground(tui.Color)).
Text styles
These classes accumulate and combine into a single tui.WithTextStyle(tui.NewStyle().Method1().Method2()...) call.
| Class | Style method |
|---|---|
font-bold |
.Bold() |
font-dim |
.Dim() |
text-dim |
.Dim() |
italic |
.Italic() |
underline |
.Underline() |
strikethrough |
.Strikethrough() |
reverse |
.Reverse() |
blink |
.Blink() |
Text colors
Standard: text-red, text-green, text-blue, text-cyan, text-magenta, text-yellow, text-white, text-black.
Bright: text-bright-red, text-bright-green, text-bright-blue, text-bright-cyan, text-bright-magenta, text-bright-yellow, text-bright-white, text-bright-black.
Each adds .Foreground(tui.Color) to the text style.
Background colors
Standard: bg-red, bg-green, bg-blue, bg-cyan, bg-magenta, bg-yellow, bg-white, bg-black.
Bright: bg-bright-red, bg-bright-green, bg-bright-blue, bg-bright-cyan, bg-bright-magenta, bg-bright-yellow, bg-bright-white, bg-bright-black.
Each generates tui.WithBackground(tui.NewStyle().Background(tui.Color)).
Arbitrary hex colors
Use bracket syntax for hex color values:
| Class | Description |
|---|---|
text-[#RGB] or text-[#RRGGBB] |
Text color from hex |
bg-[#RGB] or bg-[#RRGGBB] |
Background from hex |
border-[#RGB] or border-[#RRGGBB] |
Border color from hex |
Gradients
Apply color gradients to text, backgrounds, or borders.
Syntax: {target}-gradient-{start}-{end}[-direction]
Targets: text-gradient, bg-gradient, border-gradient
Directions:
-h-- horizontal (default)-v-- vertical-dd-- diagonal down-du-- diagonal up
Examples:
| Class | Generated option |
|---|---|
text-gradient-red-blue |
tui.WithTextGradient(tui.NewGradient(tui.ANSIColor(tui.Red), tui.ANSIColor(tui.Blue))) |
text-gradient-cyan-magenta-v |
Same with .WithDirection(tui.GradientVertical) |
bg-gradient-green-yellow-dd |
tui.WithBackgroundGradient(...) with diagonal down |
border-gradient-red-blue |
tui.WithBorderGradient(...) |
Scroll
| Class | Generated option |
|---|---|
overflow-scroll |
tui.WithScrollable(tui.ScrollBoth) |
overflow-y-scroll |
tui.WithScrollable(tui.ScrollVertical) |
overflow-x-scroll |
tui.WithScrollable(tui.ScrollHorizontal) |
overflow-hidden |
tui.WithOverflow(tui.OverflowHidden) |
scrollbar-hidden |
tui.WithScrollbarHidden(true) |
Scrollbar colors
Track: scrollbar-red, scrollbar-green, etc. (all 16 standard and bright colors).
Thumb: scrollbar-thumb-red, scrollbar-thumb-green, etc. (all 16 standard and bright colors).
Visibility and behavior
| Class | Generated option |
|---|---|
focusable |
tui.WithFocusable(true) |
hidden |
tui.WithHidden(true) |
truncate |
tui.WithTruncate(true) |
Code generation
Commands
tui generate [path...] # generate _gsx.go from .gsx files
tui check [path...] # validate .gsx without generating
tui fmt [path...] # format .gsx files
tui fmt --check [path...] # check formatting without modifying
How generation works
- The compiler lexes and parses each
.gsxfile into an AST. - The analyzer validates element tags, attributes, ref usage, and Tailwind classes.
- The generator produces a
_gsx.gofile in the same directory with the same package name. - Each
templblock becomes a Go function or method returning*tui.Element. - Elements become calls to
tui.New(options...)withAddChildcalls for children. - Tailwind classes become element option arguments at compile time (not at runtime).
- Control flow (
if,for,:=) becomes standard Go control flow.
Re-run tui generate after any .gsx change. The generated _gsx.go files should be committed to version control but never edited by hand.