1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
|
# GSX Syntax
## Overview
`.gsx` files are Go files extended with a templ-like syntax for declaring UI. They look like regular Go with the same package declarations, imports, and type definitions, but they add a `templ` keyword for defining components that return element trees. The `tui generate` command reads `.gsx` files and produces standard `_gsx.go` files that call the `tui` package API. You never need to edit the generated files.
## Package and Imports
A `.gsx` file starts with a package declaration and imports, just like any Go file:
```gsx
package main
import (
"fmt"
"strings"
tui "github.com/grindlemire/go-tui"
)
```
The examples alias the import as `tui`, but this is optional since the root package is already named `tui`. The alias just makes it explicit that option calls like `tui.NewState` use the `tui` name, even though the path ends in `go-tui`. Use whichever form you prefer.
Everything else in the file (type declarations, constants, variables, helper functions) follows normal Go syntax.
## Pure Components
Pure components are stateless functions declared with the `templ` keyword. They take parameters, return an element tree, and have no lifecycle of their own.
```gsx
templ Greeting(name string) {
<span class="text-cyan font-bold">{"Hello, " + name}</span>
}
```
This generates a function called `Greeting` that accepts `name string` and returns a `GreetingView` containing the element tree. You call it from other templates using the `@` prefix with positional arguments:
```gsx
templ App() {
<div class="flex-col gap-1">
@Greeting("Alice")
@Greeting("Bob")
</div>
}
```
### Children Slot
Both pure and struct components can accept nested content via the `{children...}` placeholder.
In a pure component, children arrive as a function parameter:
```gsx
templ Card(title string) {
<div class="border-rounded p-1 flex-col gap-1">
<span class="text-gradient-cyan-magenta font-bold">{title}</span>
<hr class="border-single" />
{children...}
</div>
}
```
The caller passes children by nesting elements inside the component call:
```gsx
templ Dashboard() {
@Card("System Info") {
<span>Version: 1.2.0</span>
<span>Uptime: 3d 14h</span>
}
}
```
Struct components use the same `{children...}` syntax. Add a `children []*tui.Element` field to the struct and accept it in the constructor:
```gsx
type panel struct {
title string
children []*tui.Element
}
func NewPanel(title string, children []*tui.Element) *panel {
return &panel{title: title, children: children}
}
templ (p *panel) Render() {
<div class="border-rounded p-1 flex-col gap-1">
<span class="font-bold">{p.title}</span>
{children...}
</div>
}
```
The caller syntax is the same as with pure components:
```gsx
templ (a *myApp) Render() {
@NewPanel("Items") {
<span>First</span>
<span>Second</span>
}
}
```
The generated code passes children through the constructor. On re-renders, `UpdateProps` copies the fresh children to the cached instance automatically.
### When to Use Pure Components
Use pure components for reusable visual elements that don't need their own state: cards, badges, headers, layout wrappers, styled containers. They're the go-tui equivalent of a React functional component with no hooks.
## Struct Components
Struct components carry state, handle input, and support lifecycle hooks. They're defined in three parts: a struct, a constructor, and a `templ` render method.
```gsx
package main
import (
"fmt"
tui "github.com/grindlemire/go-tui"
)
type counter struct {
count *tui.State[int]
}
func Counter() *counter {
return &counter{
count: tui.NewState(0),
}
}
func (c *counter) KeyMap() tui.KeyMap {
return tui.KeyMap{
tui.On(tui.KeyEscape, func(ke tui.KeyEvent) { ke.App().Stop() }),
tui.On(tui.Rune('+'), func(ke tui.KeyEvent) {
c.count.Update(func(v int) int { return v + 1 })
}),
tui.On(tui.Rune('-'), func(ke tui.KeyEvent) {
c.count.Update(func(v int) int { return v - 1 })
}),
}
}
templ (c *counter) Render() {
<div class="flex-col items-center justify-center h-full gap-1">
<span class="font-bold text-cyan">{fmt.Sprintf("Count: %d", c.count.Get())}</span>
<span class="font-dim">Press + / - to change, Esc to quit</span>
</div>
}
```
The `templ (c *counter) Render()` syntax declares a method on the struct that returns a `*tui.Element`. The method name must be `Render`. In the `.gsx` file you write no parameters, but the generated Go code adds `app *tui.App` automatically.
### The Component Interface
At minimum, a struct component must implement `Render(app *App) *Element`. The `templ` method syntax generates this for you. Beyond that, several optional interfaces add behavior:
| Interface | Method | Purpose |
|-----------|--------|---------|
| `KeyListener` | `KeyMap() tui.KeyMap` | Keyboard bindings |
| `MouseListener` | `HandleMouse(MouseEvent) bool` | Mouse click/scroll handling |
| `WatcherProvider` | `Watchers() []tui.Watcher` | Timers and channel listeners |
| `Initializer` | `Init() func()` | Setup; returns a cleanup function |
| `AppBinder` | `BindApp(app *App)` | Receive the App instance |
| `PropsUpdater` | `UpdateProps(fresh Component)` | Handle prop changes on re-mount |
You don't need to implement all of these. Use only what your component needs. The [Components](components) guide covers each in detail.
## Elements
Elements are HTML-like tags that map to `tui.Element` instances. There are two kinds: container elements that can hold children, and self-closing (void) elements that cannot.
### Container Elements
| Element | Description |
|---------|-------------|
| `<div>` | Block flex container (the workhorse for layout) |
| `<span>` | Inline text container |
| `<p>` | Paragraph with text wrapping |
| `<ul>` | Unordered list container |
| `<li>` | List item (renders with a bullet) |
| `<button>` | Clickable element |
| `<table>` | Table container |
### Self-Closing (Void) Elements
These must use the `/>` closing syntax and cannot have children:
| Element | Description |
|---------|-------------|
| `<input />` | Text input field |
| `<progress />` | Progress bar |
| `<hr />` | Horizontal rule |
| `<br />` | Line break |
### Nesting
Elements nest naturally:
```gsx
<div class="flex-col gap-1 p-1">
<div class="flex justify-between">
<span class="font-bold">Title</span>
<span class="font-dim">Subtitle</span>
</div>
<hr />
<ul class="flex-col">
<li><span>First item</span></li>
<li><span>Second item</span></li>
</ul>
</div>
```
## Attributes
Attributes set element properties. There are three forms:
### String Attributes
Quoted strings for `class`, `id`, and other string-typed properties:
```gsx
<div class="flex-col gap-2" id="main-panel">
<span class="font-bold text-cyan">Title</span>
</div>
```
### Go Expression Attributes
Curly braces for any Go expression:
```gsx
<div width={42} height={10} flexGrow={1.5}>
<span textStyle={tui.NewStyle().Bold().Foreground(tui.ANSIColor(tui.Cyan))}>
Styled text
</span>
</div>
```
This works for integers, floats, booleans, function calls, struct literals, or any other expression that produces a value of the right type.
### Boolean Attributes
A bare attribute name is shorthand for `true`:
```gsx
<button disabled>Can't click</button>
// equivalent to:
<button disabled={true}>Can't click</button>
```
### Ref Attributes
Bind an element to a reference variable for later access (scroll control, click detection, etc.):
```gsx
<div ref={myRef} class="flex-col">
content
</div>
```
See the [Event Handling](events) guide for how refs work with click handling.
### Attribute Reference
Here's the full set of supported attributes, grouped by purpose:
**Identity**: `id`, `class`, `disabled`, `ref`, `deps`
**Dimensions**: `width`, `widthPercent`, `height`, `heightPercent`, `minWidth`, `minHeight`, `maxWidth`, `maxHeight`
**Flex Container**: `direction`, `justify`, `align`, `gap`
**Flex Item**: `flexGrow`, `flexShrink`, `alignSelf`
**Spacing**: `padding`, `margin`
**Visual**: `border`, `borderStyle`, `borderTitle`, `background`, `text`, `textStyle`, `textAlign`
**Focus**: `focusable`, `onFocus`, `onBlur`
**Scroll**: `scrollable`, `scrollOffset`, `scrollbarStyle`, `scrollbarThumbStyle`
**Input-specific**: `value`, `placeholder`
**Progress-specific**: `value`, `max`
## Go Expressions
Curly braces embed Go expressions as text content or attribute values.
### Text Content
Any Go expression inside `{...}` is rendered as text:
```gsx
<span>{fmt.Sprintf("Count: %d", c.count.Get())}</span>
<span>{"Hello, " + name}</span>
<span>{len(items)}</span>
```
### Computed Classes
Classes can be built from Go expressions too:
```gsx
<span class={statusClass(isOnline)}>Status</span>
```
Where `statusClass` is a regular Go function:
```go
func statusClass(online bool) string {
if online {
return "text-green font-bold"
}
return "text-red font-dim"
}
```
### Method Calls
Call methods on the receiver or on state variables:
```gsx
<span textStyle={s.getHeaderStyle()}>{s.count.Get()}</span>
```
## Control Flow
Three directives control rendering logic: `if`, `for`, and `:=` bindings.
### if / else
Conditionally render elements based on a Go boolean expression:
```gsx
if s.count.Get() > 0 {
<span class="text-green">Positive</span>
} else {
<span class="text-red">Zero or negative</span>
}
```
You can chain conditions with `else if`:
```gsx
if s.count.Get() > 10 {
<span class="text-green font-bold">High</span>
} else if s.count.Get() > 0 {
<span class="text-yellow">Low</span>
} else {
<span class="text-red">Zero</span>
}
```
### for
Loop over slices, maps, or any Go iterable with `range`:
```gsx
for i, item := range items {
<span>{fmt.Sprintf("%d. %s", i+1, item)}</span>
}
```
You can ignore the index with `_`:
```gsx
for _, item := range items {
<span>{item}</span>
}
```
Loops and conditionals nest freely:
```gsx
for i, item := range items {
if i == s.selected.Get() {
<span class="text-cyan font-bold">{"> " + item}</span>
} else {
<span>{" " + item}</span>
}
}
```
### Element Bindings (:=)
Bind an element to a local variable to avoid repeating complex expressions:
```gsx
countText := <span class="font-bold">{fmt.Sprintf("%d", s.count.Get())}</span>
<div class="flex gap-1">
<span>Count:</span>
{countText}
</div>
```
This is useful when you want to compute an element once and reuse it. The variable is scoped to the rest of the component body after its declaration.
## Helper Functions
Regular Go functions in `.gsx` files work exactly as they do in `.go` files. They're useful for formatting, style computation, and shared logic:
```gsx
package main
import "fmt"
func keyLabel(name string, pressed bool) string {
if pressed {
return "* " + name
}
return " " + name
}
func keyStyle(pressed bool) string {
if pressed {
return "text-green font-bold"
}
return "font-dim"
}
templ KeyIndicator(name string, pressed bool) {
<span class={keyStyle(pressed)}>{keyLabel(name, pressed)}</span>
}
```
The distinction between a helper function and a component is the `templ` keyword. A `templ` declaration produces an element tree. A `func` declaration is plain Go.
## Calling Components
Components are called with the `@` prefix. Parameters are passed as positional arguments matching the component's parameter list.
### Pure Components
```gsx
templ Badge(label string) {
<span class="bg-cyan text-black px-1 font-bold">{label}</span>
}
templ StatusLine(label string, value string) {
<div class="flex gap-1">
<span class="font-dim">{label}</span>
<span>{value}</span>
</div>
}
// Usage:
templ Header() {
<div class="flex-col gap-1">
@Badge("v1.0")
@StatusLine("Status:", "Running")
</div>
}
```
### Struct Components
Struct components are instantiated through their constructor and passed to the template with `@`:
```gsx
templ (a *app) Render() {
<div class="flex h-full">
@Sidebar(a.category)
@Content(a.category, a.query)
</div>
}
```
Where `Sidebar` and `Content` are constructors that return struct component instances.
## Code Generation
After writing or editing `.gsx` files, run the code generator to produce the corresponding Go files:
```bash
tui generate ./...
```
This processes all `.gsx` files recursively and writes `_gsx.go` files alongside them. For example, `hello.gsx` produces `hello_gsx.go`. Hyphens in filenames become underscores (`my-app.gsx` becomes `my_app_gsx.go`).
The generated files should not be edited by hand. They're overwritten on every run of `tui generate`.
### Related Commands
| Command | Purpose |
|---------|---------|
| `tui generate [path...]` | Generate Go code from `.gsx` files |
| `tui check [path...]` | Validate `.gsx` files without writing output |
| `tui fmt [path...]` | Format `.gsx` files (like `gofmt` for `.gsx`) |
| `tui fmt --check [path...]` | Check formatting without modifying files |
| `tui fmt --stdout [path...]` | Write formatted output to stdout |
Path arguments accept specific files (`hello.gsx`), directories (`./examples/`), or recursive patterns (`./...`).
See the CLI section above for the full command reference.
## Putting It All Together
Here's a complete `.gsx` file that uses most of the syntax covered above: pure components, a struct component with state, control flow, helper functions, and children slots:
```gsx
package main
import (
"fmt"
tui "github.com/grindlemire/go-tui"
)
// Helper function: formats an item label
func itemLabel(index int, name string, selected bool) string {
prefix := " "
if selected {
prefix = "> "
}
return fmt.Sprintf("%s%d. %s", prefix, index+1, name)
}
// Helper function: returns a style class based on selection
func itemClass(selected bool) string {
if selected {
return "text-cyan font-bold"
}
return ""
}
// Pure component with children slot
templ Panel(title string) {
<div class="border-rounded p-1 flex-col gap-1" width={32}>
<span class="font-bold text-gradient-cyan-magenta">{title}</span>
<hr />
{children...}
</div>
}
// Struct component with state and key handling
type listApp struct {
items []string
selected *tui.State[int]
}
func ListApp() *listApp {
return &listApp{
items: []string{"Alpha", "Bravo", "Charlie", "Delta"},
selected: tui.NewState(0),
}
}
func (l *listApp) KeyMap() tui.KeyMap {
return tui.KeyMap{
tui.On(tui.KeyEscape, func(ke tui.KeyEvent) { ke.App().Stop() }),
tui.On(tui.KeyUp, func(ke tui.KeyEvent) {
l.selected.Update(func(v int) int {
if v > 0 {
return v - 1
}
return v
})
}),
tui.On(tui.KeyDown, func(ke tui.KeyEvent) {
l.selected.Update(func(v int) int {
if v < len(l.items)-1 {
return v + 1
}
return v
})
}),
}
}
templ (l *listApp) Render() {
<div class="flex-col items-center justify-center h-full">
@Panel("Select an Item") {
for i, item := range l.items {
<span class={itemClass(i == l.selected.Get())}>
{itemLabel(i, item, i == l.selected.Get())}
</span>
}
<br />
if l.selected.Get() >= 0 {
<span class="font-dim">{fmt.Sprintf("Selected: %s", l.items[l.selected.Get()])}</span>
}
}
</div>
}
```
With a corresponding `main.go`:
```go
package main
import (
"fmt"
"os"
tui "github.com/grindlemire/go-tui"
)
func main() {
app, err := tui.NewApp(
tui.WithRootComponent(ListApp()),
)
if err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
defer app.Close()
if err := app.Run(); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}
```
Generate and run:
```bash
tui generate ./...
go run .
```
Here's what the finished list looks like:

## Next Steps
- [Styling and Colors](styling) โ Text styles, colors, borders, and gradients
- [Layout](layout) โ Flexbox layout: direction, alignment, spacing, and sizing
- [Components](components) โ Component patterns, composition, and lifecycle interfaces
|