gitstack

grindlemire/go-tui code browser

24.8 KB markdown 786 lines 2026-06-13 · 488fbfe raw

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 package declaration and import block (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>
}

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

  1. The compiler lexes and parses each .gsx file into an AST.
  2. The analyzer validates element tags, attributes, ref usage, and Tailwind classes.
  3. The generator produces a _gsx.go file in the same directory with the same package name.
  4. Each templ block becomes a Go function or method returning *tui.Element.
  5. Elements become calls to tui.New(options...) with AddChild calls for children.
  6. Tailwind classes become element option arguments at compile time (not at runtime).
  7. 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.