Go-native terminal UI · running live in this browser

Build TUIs
like ordinary Go.

Mutate one state struct. Handle events directly. Render one frame. No Msg/Cmd/Update loop, no message plumbing — just the program you'd write anyway.

flat-landing · showcase live · wasm
 flatte · compiling WebAssembly
the same Go core that renders to your terminal

Same modal. Half the ceremony.

A background ticker plus a keypress — the exact cmd/bubble-modal and cmd/flat-modal samples in this repo, side by side.

Bubble Tea bubble-modal/main.go
// a message type, just to say "time passed"
type tickMsg struct{}

func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    switch msg.(type) {
    case tickMsg:
        m.ticks++
        return m, tickCmd()   // reschedule via Cmd
    case tea.KeyMsg:
        return m.updateKey(msg) // (Model, Cmd)
    }
    return m, nil          // copy back the model
}
Flatte flat-modal/main.go
// no message type — just a named update
func applyTick(s *State, _ time.Time) {
    s.ticks++                    // mutate in place
}

func Init(s *State, fx flatte.Effects[State]) {
    flatte.Every(fx, "tick", ival, applyTick)
}

func Handle(s *State, ev flatte.Event, fx …) {
    if key.Key == flatte.KeyEnter {
        s.modalOpen = true       // no return, no Cmd
    }
}
01

Direct mutation

Handle(state, event) writes to your struct in place. No dispatch, no reducer, no round trip through a message type.

02

Pure views

View(state) returns a Frame. Same input, same pixels — every screen is a golden snapshot you can test.

03

One writer loop

Async work is a goroutine that sends one named StateUpdate back. No mutexes, no goroutine-per-widget.