gova-declarative-gui

v2026.09.24

Build native desktop apps in Go using Gova's declarative, component-based GUI framework with reactive state and platform-native integrations.

GitHub
Install command
npx skhub add reason-machines/gova-declarative-gui
Markdown
SKILL.md

Gova Declarative GUI Framework

Skill by ara.so — Daily 2026 Skills collection.

Gova is a declarative GUI framework for Go that builds native desktop apps for macOS, Windows, and Linux from a single codebase. Views are plain Go structs, state is explicit via a Scope, and go build produces one static binary. Internally powered by Fyne (BSD-3), but the public API is stable and renderer-independent.


Install

go get github.com/nv404/gova@latest

Optional CLI:

go install github.com/nv404/gova/cmd/gova@latest

Prerequisites:

  • Go 1.26+
  • C toolchain: Xcode CLT (macOS), build-essential + libgl1-mesa-dev (Linux), MinGW (Windows)

Key CLI Commands

CommandPurpose
gova dev ./path/to/appHot reload — watch .go files, rebuild and relaunch on save
gova build ./path/to/appCompile to ./bin/<name> static binary
gova run ./path/to/appBuild and launch once, no file watching
go build -ldflags "-s -w"Stripped binary (~23 MB for simple apps)

Core Concepts

1. Components as Structs

A component is a Go struct implementing Body(s *g.Scope) g.View. Fields on the struct are typed props; zero values are defaults.

package main

import g "github.com/nv404/gova"

type Greeting struct {
    Name string // prop with zero-value default ""
}

func (c Greeting) Body(s *g.Scope) g.View {
    name := c.Name
    if name == "" {
        name = "World"
    }
    return g.Text("Hello, " + name + "!").Font(g.Title)
}

2. Reactive State with Scope

State lives on the *g.Scope passed to Body. No hidden scheduler, no hook-ordering rules.

func (Counter) Body(s *g.Scope) g.View {
    count := g.State(s, 0) // typed signal, initial value 0

    return g.VStack(
        g.Text(count.Format("Count: %d")).Font(g.Title),
        g.HStack(
            g.Button("-", func() { count.Set(count.Get() - 1) }),
            g.Button("+", func() { count.Set(count.Get() + 1) }),
        ).Spacing(g.SpaceMD),
    ).Padding(g.SpaceLG)
}

3. Entry Point

func main() {
    g.Run("My App", g.Component(MyComponent{}))
}

Layout Primitives

// Vertical stack
g.VStack(child1, child2, child3).Spacing(g.SpaceMD).Padding(g.SpaceLG)

// Horizontal stack
g.HStack(child1, child2).Spacing(g.SpaceSM)

// Layered/overlapping stack
g.ZStack(background, foreground)

// Scaffold (app shell with nav, toolbar, etc.)
g.Scaffold(
    g.NavBar("Title"),
    content,
)

Spacing Constants

ConstantUse
g.SpaceSMSmall gaps
g.SpaceMDMedium gaps
g.SpaceLGLarge padding

Built-in Views

g.Text("Hello").Font(g.Title)        // styled text
g.Text("body text").Font(g.Body)

g.Button("Click me", func() { /* handler */ })

g.TextField(value.Get(), func(s string) { value.Set(s) })

g.Toggle(enabled.Get(), func(b bool) { enabled.Set(b) })

g.Image("path/to/image.png")

g.Spacer() // flexible space
g.Divider()

Font Constants

g.Title, g.Headline, g.Body, g.Caption, g.Mono


State Patterns

Basic State

count := g.State(s, 0)
count.Get()        // read
count.Set(42)      // write, triggers re-render
count.Format("Value: %d") // returns formatted string signal

Derived / Computed State

doubled := g.Derived(s, func() int {
    return count.Get() * 2
})

Effects (side effects on state change)

g.Effect(s, func() {
    fmt.Println("count changed to", count.Get())
}, count) // dependencies

Persisted State (survives hot reload)

name := g.PersistedState(s, "user-name", "")

Full Example: Todo App

package main

import g "github.com/nv404/gova"

type Todo struct {
    Text string
    Done bool
}

type TodoApp struct{}

func (TodoApp) Body(s *g.Scope) g.View {
    todos := g.State(s, []Todo{})
    input := g.State(s, "")

    addTodo := func() {
        if input.Get() == "" {
            return
        }
        todos.Set(append(todos.Get(), Todo{Text: input.Get()}))
        input.Set("")
    }

    rows := make([]g.View, 0, len(todos.Get()))
    for i, todo := range todos.Get() {
        i, todo := i, todo // capture loop vars
        rows = append(rows, g.HStack(
            g.Toggle(todo.Done, func(v bool) {
                list := todos.Get()
                list[i].Done = v
                todos.Set(list)
            }),
            g.Text(todo.Text),
        ).Spacing(g.SpaceSM))
    }

    return g.VStack(
        g.Text("Todos").Font(g.Title),
        g.VStack(rows...).Spacing(g.SpaceSM),
        g.HStack(
            g.TextField(input.Get(), func(v string) { input.Set(v) }),
            g.Button("Add", addTodo),
        ).Spacing(g.SpaceSM),
    ).Padding(g.SpaceLG)
}

func main() {
    g.Run("Todo", g.Component(TodoApp{}))
}

Native Dialogs (macOS: NSAlert / NSOpenPanel; other platforms: Fyne fallback)

// Alert dialog
g.Button("Alert", func() {
    g.Alert(g.AlertOptions{
        Title:   "Warning",
        Message: "Something happened.",
        Style:   g.AlertWarning,
    })
})

// Open file dialog
g.Button("Open File", func() {
    path, err := g.OpenFileDialog(g.OpenFileOptions{
        Title:      "Choose a file",
        Extensions: []string{".txt", ".md"},
    })
    if err == nil && path != "" {
        filePath.Set(path)
    }
})

// Save file dialog
g.Button("Save", func() {
    dest, err := g.SaveFileDialog(g.SaveFileOptions{
        Title:           "Save As",
        DefaultFilename: "output.txt",
    })
    if err == nil && dest != "" {
        // write to dest
    }
})

Platform Integration (macOS Dock)

// Dock badge (macOS)
g.DockBadge("3")
g.DockBadge("") // clear badge

// Dock progress (macOS)
g.DockProgress(0.75) // 0.0–1.0
g.DockProgress(-1)   // hide

// Dock menu (macOS)
g.SetDockMenu([]g.MenuItem{
    {Label: "New Window", Action: func() { /* ... */ }},
    {Label: "Preferences", Action: func() { /* ... */ }},
})

Theming and Colors

// Dark/light toggle
g.Button("Toggle Theme", func() {
    if g.CurrentTheme() == g.ThemeDark {
        g.SetTheme(g.ThemeLight)
    } else {
        g.SetTheme(g.ThemeDark)
    }
})

// Semantic colors in custom views
g.Text("Primary").Color(g.ColorPrimary)
g.Text("Secondary").Color(g.ColorSecondary)
g.Text("Danger").Color(g.ColorDanger)

Component Composition with Viewable

type Card struct {
    Title   string
    Content g.View
}

func (c Card) Body(s *g.Scope) g.View {
    return g.VStack(
        g.Text(c.Title).Font(g.Headline),
        g.Divider(),
        c.Content,
    ).Padding(g.SpaceMD)
}

// Usage
g.Component(Card{
    Title:   "My Card",
    Content: g.Text("Card body text"),
})

Navigation / Multi-View

type NotesApp struct{}

func (NotesApp) Body(s *g.Scope) g.View {
    selected := g.State(s, "list")

    switch selected.Get() {
    case "detail":
        return g.Component(DetailView{OnBack: func() { selected.Set("list") }})
    default:
        return g.Component(ListView{OnSelect: func() { selected.Set("detail") }})
    }
}

App Icon at Runtime

func main() {
    app := g.NewApp("My App")
    app.SetIcon("assets/icon.png") // set before Run
    app.Run(g.Component(MyComponent{}))
}

Hot Reload with PersistedState

When using gova dev, use g.PersistedState to keep UI state across rebuilds:

func (MyApp) Body(s *g.Scope) g.View {
    // survives hot reload, lost on full restart
    activeTab := g.PersistedState(s, "active-tab", "home")
    // ...
}

Project Structure (recommended)

myapp/
├── main.go          # g.Run entry point
├── components/
│   ├── header.go
│   └── sidebar.go
├── views/
│   ├── home.go
│   └── settings.go
├── assets/
│   └── icon.png
└── go.mod

Platform Support Matrix

FeaturemacOSWindowsLinux
Core UI✅✅✅
Hot reload✅✅✅
App icon✅✅✅
Native dialogsNSAlert/NSOpenPanelFyne fallbackFyne fallback
Dock/taskbarNSDockTile ✅PlannedPlanned

Troubleshooting

cgo: C compiler not found

  • macOS: xcode-select --install
  • Linux: sudo apt install build-essential libgl1-mesa-dev
  • Windows: Install MinGW-w64 and add to PATH

go build fails with OpenGL errors on Linux

sudo apt install libgl1-mesa-dev xorg-dev

Hot reload not picking up changes

  • Ensure you're using gova dev, not go run
  • Confirm .go files are in a directory watched by gova dev ./path

Binary is large (~32 MB)

  • Strip symbols: go build -ldflags "-s -w" -o ./bin/myapp
  • Result: ~23 MB. This is expected — Fyne (OpenGL renderer) is bundled.

State not updating the UI

  • Always call count.Set(...) — mutating a slice/map in place without Set won't trigger re-render
  • For slices: copy, modify, then set: list := todos.Get(); list[i] = newVal; todos.Set(list)

API breakage (pre-1.0)

  • Pin a specific tag: go get github.com/nv404/gova@v0.x.y
  • Check the CHANGELOG before upgrading

Useful Links

Discovery
Tags

No tags published for this skill.

Version
Latest version metadata

Version

v2026.09.24

Published

Sep 24, 2026

Category

Uncategorized

License

Not specified

Source path

skills/gova-declarative-gui

Default branch

main

Latest commit

2384a00

Tree SHA

ac47bb5