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
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
|
# Components
## Overview
go-tui has two kinds of components: pure templ functions and struct components.
Pure templ functions are stateless. They take parameters, return elements, and that's it. Use them for reusable visual pieces like cards, badges, and headers.
Struct components carry state. They own `State[T]` fields, handle keyboard input, run timers, and manage their own lifecycle. Use them for anything interactive or anything that changes over time.
Both kinds compose freely. A struct component's render method can call pure components, and pure components can nest other pure components inside themselves. You build complex UIs by stacking simple pieces.
## Pure Components
A pure component is a `templ` function. It takes parameters and returns a chunk of UI:
```gsx
package main
import tui "github.com/grindlemire/go-tui"
templ Badge(label string) {
<span class="text-cyan font-bold px-1">{label}</span>
}
templ Header(title string) {
<div class="border-rounded p-1 flex justify-center">
<span class="text-gradient-cyan-magenta font-bold">{title}</span>
</div>
}
templ StatusLine(label string, value string) {
<div class="flex gap-1">
<span class="font-dim">{label}</span>
<span class="text-cyan font-bold">{value}</span>
</div>
}
```
Call them from other templates just like HTML elements, using `@`:
```gsx
templ (a *myApp) Render() {
<div class="flex-col gap-1 p-1">
@Header("Dashboard")
@Badge("v1.0")
@StatusLine("Status:", "Online")
</div>
}
```
Parameters can be any Go type: strings, ints, booleans, slices, custom structs. The only rule is that pure components don't hold state between renders. Every call constructs a fresh view from the parameters you give it.
### Children Slot
Pure components can accept nested content through the `{children...}` slot. This lets you build wrapper components that add layout, borders, or styling around arbitrary content:
```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>
}
```
Pass children by nesting elements inside the component call:
```gsx
templ (a *myApp) Render() {
<div class="flex gap-2">
@Card("System Info") {
@StatusLine("Version:", "1.2.0")
@StatusLine("Uptime:", "3d 14h")
}
@Card("Config") {
@StatusLine("Theme:", "Dark")
@StatusLine("Notify:", "On")
}
</div>
}
```
Each `Card` renders its title and divider, then places whatever children you passed where `{children...}` appears. The children can be elements, other pure components, or any mix.
`tui generate` compiles the children into a `[]*tui.Element` slice and passes it as a parameter to the generated function. The slot expands to a loop that adds each child element to the parent container.
Struct components also support `{children...}`. See [Children Slot](#children-slot-2) in the struct components section below.
### When to Use Pure Components
Use pure components for anything that doesn't need its own state or event handlers:
- Visual elements: cards, badges, headers, dividers
- Layout wrappers: bordered sections, padded containers, centered panels
- Data display: status lines, key-value pairs, formatted labels
- Repeated patterns: list items, table rows, form fields
If you find yourself wanting to add a `*tui.State` field, you need a struct component instead.
## Struct Components
Struct components are Go structs with a `templ` render method. They hold state, handle input, and manage their own lifecycle:
```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 p-1 border-rounded border-cyan items-center gap-1">
<span class="font-bold">Counter</span>
<span class="text-cyan font-bold">{fmt.Sprintf("%d", c.count.Get())}</span>
<span class="font-dim">+/- to change, esc to quit</span>
</div>
}
```
There are three parts: the struct with state fields, a constructor function that initializes it, and the render method using `templ (receiver) Render()` syntax.
The render method works exactly like a pure templ function, but it has access to the struct's fields through the receiver. The generated code turns it into a `Render(app *App) *Element` method, which satisfies the `Component` interface.
### Children Slot
Struct components can also accept children using `{children...}`. 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 text-gradient-cyan-magenta">{p.title}</span>
{children...}
</div>
}
```
Callers use the same block syntax as pure components:
```gsx
templ (a *myApp) Render() {
@NewPanel("Items") {
<span>First item</span>
<span>Second item</span>
}
}
```
The generated code builds a `[]*tui.Element` slice from the children block and passes it as the last constructor argument. On re-renders, `UpdateProps` copies the fresh children to the cached instance automatically, so the panel always reflects the latest content.
This is useful when you need a wrapper component that carries its own state (timers, scroll position, internal selections) while still accepting arbitrary content from the parent.
### The Component Interface
Every struct component implements this interface:
```go
type Component interface {
Render(app *App) *Element
}
```
You don't write `Render(app *App) *Element` by hand. The `templ` keyword handles the signature. You write `templ (c *counter) Render()` and `tui generate` produces the correct Go method.
## Component Interfaces
Struct components can implement additional interfaces for input handling, background tasks, and lifecycle hooks. All are optional. Implement only what you need.
### KeyListener
Handle keyboard input by implementing `KeyMap() tui.KeyMap`:
```go
type KeyListener interface {
KeyMap() KeyMap
}
```
`KeyMap()` returns a slice of key bindings. The framework calls it on every render pass, so you can return different bindings based on current state:
```go
func (s *search) KeyMap() tui.KeyMap {
if s.active.Get() {
return tui.KeyMap{
tui.On(tui.KeyEscape, func(ke tui.KeyEvent) { s.active.Set(false) }),
tui.OnStop(tui.AnyRune,func(ke tui.KeyEvent) {
s.query.Update(func(q string) string { return q + string(ke.Rune) })
}),
}
}
return tui.KeyMap{
tui.On(tui.Rune('/'), func(ke tui.KeyEvent) { s.active.Set(true) }),
}
}
```
### MouseListener
Handle mouse input by implementing `HandleMouse(MouseEvent) bool`:
```go
type MouseListener interface {
HandleMouse(MouseEvent) bool
}
```
Return `true` if you handled the event, `false` to let it propagate. The typical pattern uses refs and `HandleClicks`:
```go
func (c *counter) HandleMouse(me tui.MouseEvent) bool {
return tui.HandleClicks(me,
tui.Click(c.incBtn, func() {
c.count.Update(func(v int) int { return v + 1 })
}),
tui.Click(c.decBtn, func() {
c.count.Update(func(v int) int { return v - 1 })
}),
)
}
```
Mouse events require `tui.WithMouse()` when creating the app. See the [Event Handling](events) guide for details.
### WatcherProvider
Run background timers or receive from Go channels by implementing `Watchers() []Watcher`:
```go
type WatcherProvider interface {
Watchers() []Watcher
}
```
Watchers start when the component mounts and stop when it unmounts. Callbacks run on the UI thread, so you can safely update state:
```go
func (t *timer) Watchers() []tui.Watcher {
return []tui.Watcher{
tui.OnTimer(time.Second, func() {
t.elapsed.Update(func(v int) int { return v + 1 })
}),
}
}
```
See [Timers, Watchers, and Channels](watchers) for the full API.
### Initializer
Run setup code when a component first mounts by implementing `Init() func()`:
```go
type Initializer interface {
Init() func()
}
```
`Init()` is called once when the component enters the tree. The returned function, if non-nil, runs when the component leaves the tree. This pairs setup and cleanup at the same call site:
```go
func (c *conn) Init() func() {
c.ws = connectWebSocket(c.url)
go c.readLoop()
return func() {
c.ws.Close()
}
}
```
### AppBinder
Wire up `State` and `Events` fields to the app by implementing `BindApp(app *App)`:
```go
type AppBinder interface {
BindApp(app *App)
}
```
You rarely write this by hand. The code generator produces `BindApp` methods automatically for any struct component that has `*tui.State` or `*tui.Events` fields. The mount system calls it when the component is first created and again after props updates.
### PropsUpdater
Receive updated props when a cached component is re-rendered:
```go
type PropsUpdater interface {
UpdateProps(fresh Component)
}
```
You never write this by hand. The code generator produces `UpdateProps` methods automatically for all components. For struct components, it copies non-state fields (skipping `*tui.State`, `*tui.Ref`, channels, and functions) from a fresh instance to the cached one. The mount system calls it on subsequent renders so the cached instance picks up any changed props without losing its internal state.
## Component Composition
Components compose by nesting. Call a pure component with `@`, and it inlines its elements into the parent. Call a struct component the same way, and the framework handles mounting and caching behind the scenes.
### Pure Components Inside Struct Renders
The most common pattern. A struct component's render method calls pure components for layout and display:
```gsx
package main
import (
"fmt"
tui "github.com/grindlemire/go-tui"
)
templ Badge(label string, style string) {
<span class={style + " font-bold px-1"}>{label}</span>
}
templ InfoRow(label string, value string) {
<div class="flex gap-1">
<span class="font-dim">{label}</span>
<span class="text-cyan font-bold">{value}</span>
</div>
}
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>
}
type userProfile struct {
name string
role string
email string
}
func UserProfile(name, role, email string) *userProfile {
return &userProfile{name: name, role: role, email: email}
}
func (u *userProfile) KeyMap() tui.KeyMap {
return tui.KeyMap{
tui.On(tui.KeyEscape, func(ke tui.KeyEvent) { ke.App().Stop() }),
}
}
templ (u *userProfile) Render() {
<div class="flex-col gap-1 p-1">
@Card("User Profile") {
<div class="flex-col gap-1">
@InfoRow("Name:", u.name)
@InfoRow("Role:", u.role)
@InfoRow("Email:", u.email)
<div class="flex gap-1">
<span class="font-dim">Status:</span>
@Badge("Active", "text-green")
</div>
</div>
}
</div>
}
```
### Struct Components Inside Struct Renders
When one struct component renders another struct component, the framework uses `app.Mount` to cache the child instance. This means the child keeps its state across parent re-renders:
```gsx
package main
import tui "github.com/grindlemire/go-tui"
type sidebar struct {
category *tui.State[string]
selected *tui.State[int]
}
func Sidebar(category *tui.State[string]) *sidebar {
return &sidebar{
category: category,
selected: tui.NewState(0),
}
}
func (s *sidebar) KeyMap() tui.KeyMap {
return tui.KeyMap{
tui.On(tui.Rune('j'), func(ke tui.KeyEvent) {
s.selected.Update(func(v int) int { return v + 1 })
}),
tui.On(tui.Rune('k'), func(ke tui.KeyEvent) {
s.selected.Update(func(v int) int { return v - 1 })
}),
}
}
templ (s *sidebar) Render() {
<div class="flex-col border-single p-1" width={20}>
<span class="font-bold text-cyan">Sidebar</span>
// ... sidebar content ...
</div>
}
type myApp struct {
category *tui.State[string]
}
func MyApp() *myApp {
return &myApp{
category: tui.NewState("Documents"),
}
}
func (a *myApp) KeyMap() tui.KeyMap {
return tui.KeyMap{
tui.On(tui.KeyEscape, func(ke tui.KeyEvent) { ke.App().Stop() }),
}
}
templ (a *myApp) Render() {
<div class="flex h-full">
@Sidebar(a.category)
<div class="flex-col flex-1 p-1">
<span>Main content</span>
</div>
</div>
}
```
When `tui generate` processes this, it turns `@Sidebar(a.category)` inside a struct render method into an `app.Mount` call:
```go
app.Mount(a, 0, func() tui.Component {
return Sidebar(a.category)
})
```
The mount system caches the `sidebar` instance by its parent and a cache key (its position in the file, combined with the loop's index or map key when mounted inside a loop). On the first render, it creates the sidebar, binds its state to the app, and calls `Init()` if implemented. On subsequent renders, it reuses the cached instance and calls `UpdateProps` if the child implements it.
The sidebar's `selected` state persists even when the parent re-renders, because the framework reuses the cached instance instead of creating a new one.
### Shared State
Parent and child components can share state by passing `*tui.State` values through constructors. Changes from either side trigger a re-render:
```gsx
type myApp struct {
category *tui.State[string]
}
func MyApp() *myApp {
return &myApp{
category: tui.NewState("Documents"),
}
}
templ (a *myApp) Render() {
<div class="flex h-full">
@Sidebar(a.category)
@Content(a.category)
</div>
}
```
Both `Sidebar` and `Content` receive the same `category` state. When the sidebar changes it, the content panel sees the new value on the next render.
## Composition Patterns
### Layout Wrapper
A pure component that wraps content in a consistent layout:
```gsx
templ PageLayout(title string) {
<div class="flex-col h-full">
<div class="flex justify-center p-1 border-single">
<span class="text-gradient-cyan-magenta font-bold">{title}</span>
</div>
<div class="flex-col flex-1 p-1">
{children...}
</div>
<div class="flex justify-center p-1 border-single">
<span class="font-dim">Press q to quit</span>
</div>
</div>
}
```
### Data Display
Pure components for showing key-value data:
```gsx
templ KeyValue(key string, value string) {
<div class="flex gap-1">
<span class="font-dim">{key + ":"}</span>
<span class="text-cyan">{value}</span>
</div>
}
templ Section(title string) {
<div class="flex-col border-rounded p-1 gap-1">
<span class="font-bold">{title}</span>
<hr class="border-single" />
{children...}
</div>
}
```
### Container Component
A reusable scrollable panel:
```gsx
templ ScrollPanel(title string, height int) {
<div class="flex-col border-rounded" height={height}>
<div class="flex p-1">
<span class="font-bold text-cyan">{title}</span>
</div>
<div class="flex-col overflow-y-scroll flex-1 p-1">
{children...}
</div>
</div>
}
```
## Complete Example
A full app with pure components, a struct component, and composition:
```gsx
package main
import (
"fmt"
tui "github.com/grindlemire/go-tui"
)
// Pure components
templ Badge(label string, color string) {
<span class={color + " font-bold px-1"}>{label}</span>
}
templ StatusLine(label string, value string) {
<div class="flex gap-1">
<span class="font-dim">{label}</span>
<span class="text-cyan font-bold">{value}</span>
</div>
}
templ Card(title string) {
<div class="border-rounded p-1 flex-col gap-1 w-full" flexGrow={1.0}>
<span class="text-gradient-cyan-magenta font-bold">{title}</span>
<hr class="border-single" />
{children...}
</div>
}
// Tab content components
templ OverviewTab() {
<div class="flex gap-1">
@Card("System") {
@StatusLine("CPU:", "42%")
@StatusLine("Memory:", "1.2 GB")
@StatusLine("Disk:", "68%")
}
@Card("Network") {
@StatusLine("In:", "12 MB/s")
@StatusLine("Out:", "3 MB/s")
@StatusLine("Latency:", "24ms")
}
</div>
}
templ MetricsTab() {
<div class="flex gap-1">
@Card("Performance") {
@StatusLine("Requests:", "1.2k/s")
@StatusLine("P99:", "145ms")
@StatusLine("Errors:", "0.02%")
}
@Card("Storage") {
@StatusLine("Used:", "42 GB")
@StatusLine("Free:", "118 GB")
@StatusLine("IOPS:", "3.4k")
}
</div>
}
templ LogsTab() {
<div class="flex gap-1">
@Card("Application") {
@StatusLine("Level:", "INFO")
@StatusLine("Rate:", "84/min")
@StatusLine("Errors:", "2")
}
@Card("Security") {
@StatusLine("Auth:", "OK")
@StatusLine("Blocked:", "17")
@StatusLine("Alerts:", "0")
}
</div>
}
// Struct component
type dashboard struct {
selected *tui.State[int]
tabs []string
}
func Dashboard() *dashboard {
return &dashboard{
selected: tui.NewState(0),
tabs: []string{"Overview", "Metrics", "Logs"},
}
}
func (d *dashboard) KeyMap() tui.KeyMap {
return tui.KeyMap{
tui.On(tui.KeyEscape, func(ke tui.KeyEvent) { ke.App().Stop() }),
tui.On(tui.KeyTab, func(ke tui.KeyEvent) {
d.selected.Update(func(v int) int {
return (v + 1) % len(d.tabs)
})
}),
}
}
templ (d *dashboard) Render() {
<div class="flex-col p-1 gap-1 border-rounded border-cyan">
<div class="flex gap-2">
for i, tab := range d.tabs {
if i == d.selected.Get() {
@Badge(tab, "text-cyan")
} else {
<span class="font-dim">{tab}</span>
}
}
</div>
if d.selected.Get() == 0 {
@OverviewTab()
} else if d.selected.Get() == 1 {
@MetricsTab()
} else {
@LogsTab()
}
<div class="flex justify-center">
<span class="font-dim">{fmt.Sprintf("Tab: switch tabs | esc: quit | Viewing: %s", d.tabs[d.selected.Get()])}</span>
</div>
</div>
}
```
With `main.go`:
```go
package main
import (
"fmt"
"os"
tui "github.com/grindlemire/go-tui"
)
func main() {
app, err := tui.NewApp(
tui.WithRootComponent(Dashboard()),
)
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 .
```
When you run it, you'll get a tabbed dashboard:

## Next Steps
- [Event Handling](events) -- Keyboard and mouse input in depth
- [Timers, Watchers, and Channels](watchers) -- Background operations and the event bus
- [State and Reactivity](state) -- Reactive state management with `State[T]`
|