Cross-platform, hybrid immediate-mode GUI framework for Go — no virtual DOM, no diffing, just fast, composable UI.
Showcase contains the framework documentation. Every widget demo has a button in the upper-right corner that displays documentation about the widget.
Sibling projects:
-
go-charts
Interactive chart widgets. https://github.com/mike-ward/go-charts -
go-edit
Code editor widget. https://github.com/mike-ward/go-edit -
go-map
SMIL map widgets. https://github.com/mike-ward/go-map -
go-glyph
Text rendering engine on steriods. https://github.com/mike-ward/go-glyph
Go-Gui is a hybrid immediate-mode GUI framework where every frame, a plain Go function returns a layout tree that gets sized, positioned, and rendered directly to the screen. State lives in a single typed slot per window.
Most GUI frameworks force a tradeoff: you either get the simplicity of immediate mode or the power of retained systems. go-gui gives you both—without the usual compromises.
Traditional immediate-mode systems like Dear ImGui are fast and simple, but struggle with state, structure, accessibility, and large-scale apps. Retained frameworks like Qt or Flutter solve those problems—but at the cost of complexity, hidden state, and rigid architectures.
go-gui bridges that gap.
-
Simple, deterministic API
Build UI like immediate mode—plain Go functions, minimal boilerplate, no surprises. -
Stable identity + reactive state
Component IDs and a typed per-window state slot give you persistence and control—without framework magic. -
Time-travel debugging
Built-in snapshot/restore ring buffer lets you scrub back through app state—deep visibility typically reserved for retained systems. -
Production-grade accessibility
Built-in ARIA roles, semantic structure, and IME support make real-world apps possible—not just demos. -
High performance at scale
No virtual DOM, no diffing. Layout, sizing, and rendering happen in a single pass each frame. Virtualization is built in where it matters—DataGrid handles large datasets with no manual tuning. -
Animation built in
A first-class animation subsystem ships with the framework—smooth transitions and motion are not bolted on after the fact. -
Rich, extensible widget ecosystem
50+ built-in widgets from data grids to code editors. Charts and maps are available as first-class companion libraries. Custom shaders let you extend rendering beyond the built-in widget set. -
Cross-platform, single binary
Desktop, web, and mobile targets with native dialogs, menus, and OS features. Compiles to a native binary—no runtime to install, no VM overhead.
The result:
A framework that keeps the clarity and speed of immediate mode, while delivering the
power, scalability, and tooling of modern UI systems.
No hidden machinery. Just control, performance, and capability.
spinners.mp4
Widgets & Layout
- 50+ built-in widgets — buttons, inputs, sliders, tables, trees, tabs, menus, dialogs, toasts, breadcrumbs, and more
- Flexible layout system — Row, Column, Wrap, Canvas, Splitter, DockLayout, ExpandPanel, OverflowPanel, RotatedBox
- DataGrid with virtualization, sorting, grouping, inline editing, CSV/TSV/XLSX/PDF export, and async data sources
- Dock layout with drag-and-drop panel rearrangement and tab groups
Rendering & Backends
- GPU-accelerated rendering via SDL2 + Metal (macOS) / OpenGL (Linux/Windows)
- Web/WASM backend — Canvas2D with custom WebGL shaders, runs in any browser
- iOS backend — Metal rendering, UIKit windowing, touch events
- Android backend support
- Custom GPU shader support
Text & Rich Content
- Rich text input — multiline, click-to-cursor, drag-to-select, word select, autoscroll, Home/End cycling
- Markdown and RTF views with syntax highlighting and code-block copy-to-clipboard
- Text selection and copy for read-only Text widgets
- SVG loading, caching, and tessellation
- Powered by go-glyph for professional-grade text shaping, rendering, and bidirectional layout
Animation & Effects
- Full animation subsystem — keyframe, spring, tween, hero transitions
- ColorFilter post-processing — grayscale, sepia, brightness, contrast, hue rotate, invert, saturation, and composable filter chains
- ClipContents — stencil-based rounded-rect clip masking
- Box shadows and blur effects
Input & Interaction
- Touch gesture recognition — tap, double-tap, long-press, pan, swipe, pinch, rotate with automatic mouse-event synthesis for compatibility
- Pan gestures auto-scroll containers; pinch/rotate available on ContainerCfg and DrawCanvasCfg
Audio
- Opt-in audio via SDL_mixer — sound effects and music playback
- Multiple mixing channels, volume control, fade in/out
- Load from file or embedded bytes (WAV, OGG, MP3, FLAC, MOD)
Platform Integration
- Windows backend — native file dialogs, print, and notifications
- OS-level spell check (macOS NSSpellChecker, Linux Hunspell)
- IME support and accessibility tree (macOS, Linux AT-SPI2)
- Multi-window support with cross-window messaging
- Native dialogs, notifications, system tray, and print/PDF
- Native menu bar support
- Locale and i18n support
- Command registry with global hotkeys and fuzzy-search command palette
Developer Experience
- Theme system with built-in dark/light variants and custom themes
- Stateless view model — views are pure functions, easy to test
- Headless test backend runs all layout and widget logic without a display
- Embedded Feather icon font with themed styles
- Time-travel debugging — opt-in
WindowCfg.DebugTimeTravelauto-spawns a scrubber window that rewinds/replays app state frame-by-frame
package main
import (
"fmt"
"github.com/mike-ward/go-gui/gui"
"github.com/mike-ward/go-gui/gui/backend"
)
type App struct{ Clicks int }
func main() {
gui.SetTheme(gui.ThemeDarkBordered)
w := gui.NewWindow(gui.WindowCfg{
State: &App{},
Title: "get_started",
Width: 300,
Height: 300,
OnInit: func(w *gui.Window) { w.UpdateView(mainView) },
})
backend.Run(w)
}
func mainView(w *gui.Window) gui.View {
ww, wh := w.WindowSize()
app := gui.State[App](w)
return gui.Column(gui.ContainerCfg{
Width: float32(ww),
Height: float32(wh),
Sizing: gui.FixedFixed,
HAlign: gui.HAlignCenter,
VAlign: gui.VAlignMiddle,
Content: []gui.View{
gui.Text(gui.TextCfg{
Text: "Hello GUI!",
TextStyle: gui.CurrentTheme().B1,
}),
gui.Button(gui.ButtonCfg{
IDFocus: 1,
Content: []gui.View{
gui.Text(gui.TextCfg{
Text: fmt.Sprintf("%d Clicks", app.Clicks),
}),
},
OnClick: func(_ *gui.Layout, e *gui.Event, w *gui.Window) {
gui.State[App](w).Clicks++
e.IsHandled = true
},
}),
},
})
}See examples/get_started/ for the full runnable
version and examples/web_demo/ for the browser build.
Go-Gui requires Go 1.26+ and SDL2 development libraries.
brew install go pkg-config sdl2 sdl2_mixer freetype harfbuzz pango fontconfigsudo apt-get update
sudo apt-get install -y \
golang build-essential pkg-config \
libsdl2-dev libsdl2-mixer-dev libfreetype6-dev libharfbuzz-dev \
libpango1.0-dev libfontconfig1-devsudo dnf install -y golang gcc pkgconf-pkg-config \
SDL2-devel SDL2_mixer-devel freetype-devel harfbuzz-devel pango-devel fontconfig-develsudo pacman -Syu --noconfirm go base-devel pkgconf \
sdl2 sdl2_mixer freetype2 harfbuzz pango fontconfigpacman -S --needed mingw-w64-x86_64-go mingw-w64-x86_64-gcc \
mingw-w64-x86_64-pkgconf mingw-w64-x86_64-SDL2 \
mingw-w64-x86_64-SDL2_mixer mingw-w64-x86_64-freetype \
mingw-w64-x86_64-harfbuzz mingw-w64-x86_64-pango \
mingw-w64-x86_64-fontconfigUse the MSYS2 MinGW x64 shell for go build / go run.
vcpkg install sdl2:x64-windows sdl2-mixer:x64-windows \
freetype:x64-windows harfbuzz:x64-windows pango:x64-windows \
fontconfig:x64-windowsSet CGO_CFLAGS and CGO_LDFLAGS to the vcpkg include/lib paths before
building.
go get github.com/mike-ward/go-guibackend.Run(w) auto-selects Metal on macOS and OpenGL elsewhere:
import "github.com/mike-ward/go-gui/gui/backend"
backend.Run(w) // Metal on macOS, GL on Linux/WindowsTo force a specific backend, import it directly:
import metal "github.com/mike-ward/go-gui/gui/backend/metal" // macOS only
import gl "github.com/mike-ward/go-gui/gui/backend/gl" // cross-platform
import sdl2 "github.com/mike-ward/go-gui/gui/backend/sdl2" // software fallback
import web "github.com/mike-ward/go-gui/gui/backend/web" // WASM/browser
import ios "github.com/mike-ward/go-gui/gui/backend/ios" // iOSgui.SetTheme(gui.ThemeDark) // set globally before NewWindow
gui.SetTheme(gui.ThemeDarkBordered) // dark with visible borders
t := gui.CurrentTheme() // read anywhereCustom themes are built with gui.ThemeMaker and registered via
gui.RegisterTheme.
app := gui.NewApp()
app.ExitMode = gui.ExitOnMainClose
w1 := gui.NewWindow(gui.WindowCfg{State: &Main{}, Title: "Main"})
w2 := gui.NewWindow(gui.WindowCfg{State: &Inspector{}, Title: "Inspector"})
backend.RunApp(app, w1, w2)Open windows at runtime with app.OpenWindow(cfg). Communicate across
windows with app.Broadcast(fn) or other.QueueCommand(fn).
Opt-in with one flag. Implement Snapshotter on your state, set
DebugTimeTravel: true, and a scrubber window spawns alongside the app.
type App struct { Count int }
func (s *App) Snapshot() any { c := *s; return &c }
func (s *App) Restore(v any) { *s = *v.(*App) }
app := gui.NewApp()
main := gui.NewWindow(gui.WindowCfg{
State: &App{},
DebugTimeTravel: true,
})
backend.RunApp(app, main)The scrubber captures state after every event. Drag the slider or
use the step buttons (or ←/→/Home/End) to rewind; Space toggles
freeze, Esc resumes live. Use w.Now() instead of time.Now() in
views whose output depends on the clock so rewound frames match their
snapshot. See examples/time_travel for a runnable demo.
Requirements: multi-window mode (App + App.OpenWindow); user state
implements Snapshotter. Zero-cost when the flag is off. Scrub is
read-only — rewinding does not undo past side effects (HTTP, file I/O).
Per-window state via a single typed slot — no globals, no closures:
type Counter struct{ N int }
w := gui.NewWindow(gui.WindowCfg{State: &Counter{}})
// Inside any callback or view:
s := gui.State[Counter](w)
s.N++Events are wired through Cfg structs:
gui.Button(gui.ButtonCfg{
IDFocus: 1,
OnClick: func(l *gui.Layout, e *gui.Event, w *gui.Window) {
// handle click
e.IsHandled = true
},
})
gui.Input(gui.InputCfg{
OnKeyDown: func(l *gui.Layout, e *gui.Event, w *gui.Window) { … },
OnCharInput: func(l *gui.Layout, e *gui.Event, w *gui.Window) { … },
OnTextCommit: func(l *gui.Layout, e *gui.Event, w *gui.Window) { … },
})35 example applications ship with the framework:
| Directory | Description |
|---|---|
animations |
Animation subsystem showcase |
benchmark |
Frame timing and allocation benchmarks |
blur_demo |
Blur visual effect |
calculator |
Styled desktop calculator |
color_picker |
Color picker widget |
command_demo |
Command registry, hotkeys, command palette |
context_menu |
Right-click context menus |
custom_shader |
Custom GPU shader rendering |
data_grid_data_source |
DataGrid with async data source |
date_picker_options |
Date picker configurations |
dialogs |
Native and custom dialogs |
digital_rain |
Matrix-style digital rain effect |
dock_layout |
IDE-style docking panels with drag-and-drop |
draw_canvas |
Custom-draw canvas surface |
floating_layout |
Float-anchored overlay positioning |
get_started |
Minimal hello-world app |
gradient_demo |
OpenGL gradient rendering |
ios_demo |
iOS demo app (Metal + UIKit) |
listbox |
ListBox widget demo |
markdown |
Markdown rendering with code-block copy |
menu_demo |
Pull-down menu bar |
multi_window |
Multi-window with cross-window messaging |
multiline_input |
Multiline text input |
native_menu |
OS-native menu bar |
rotated_box |
Quarter-turn rotation widget |
rtf |
RTF document viewer |
scroll_demo |
Scrollable content layouts |
shadow_demo |
Box shadow effects |
showcase |
Interactive widget showcase |
snake |
Snake game |
svg |
SVG loading and display |
svg_a11y |
SVG accessibility metadata (title/desc/aria) |
svg_aspect |
preserveAspectRatio alignment grid |
svg_css_selectors |
Sibling, attribute, :not() selectors |
svg_css_states |
:hover / :focus driven by SvgCfg ids |
svg_css_vars |
var() fallback + calc() arithmetic |
svg_flatness |
FlatnessTolerance vs vertex count |
svg_gradient_spread |
spreadMethod pad / reflect / repeat |
svg_hittest |
Click-hit test on tessellated SVG paths |
svg_radial |
radialGradient rendering |
svg_use_symbol |
<use> + <symbol> reuse |
system_tray |
System tray icon and menu |
time_travel |
Counter with time-travel scrubber window |
todo |
Classic todo app |
web_demo |
Browser demo via WASM |
Run any example:
go run ./examples/get_started/
go run ./examples/showcase/
go run ./examples/calculator/| Widget | Factory | Description |
|---|---|---|
| Row | Row(ContainerCfg) |
Horizontal flex container |
| Column | Column(ContainerCfg) |
Vertical flex container |
| Wrap | Wrap(ContainerCfg) |
Flow-wrap container |
| Canvas | Canvas(ContainerCfg) |
Absolute-position canvas |
| Circle | Circle(ContainerCfg) |
Circular container |
| Rectangle | Rectangle(RectangleCfg) |
Styled rectangle |
| ExpandPanel | ExpandPanel(ExpandPanelCfg) |
Collapsible section |
| Splitter | Split(SplitterCfg) |
Resizable two-pane split |
| DockLayout | DockLayout(DockCfg) |
Drag-and-drop dock areas |
| OverflowPanel | OverflowPanel(OverflowPanelCfg) |
Wraps overflowing children |
| RotatedBox | RotatedBox(RotatedBoxCfg) |
Quarter-turn rotation |
| Widget | Factory | Description |
|---|---|---|
| Button | Button(ButtonCfg) |
Clickable button |
| Input | Input(InputCfg) |
Single-line text field |
| NumericInput | NumericInput(NumericInputCfg) |
Numeric text field |
| Checkbox | Checkbox(CheckboxCfg) |
Boolean toggle |
| Radio | Radio(RadioCfg) |
Mutually-exclusive options |
| Select | Select(SelectCfg) |
Dropdown selector |
| Combobox | Combobox(ComboboxCfg) |
Editable dropdown |
| Switch | Switch(SwitchCfg) |
On/off toggle switch |
| Toggle | Toggle(ToggleCfg) |
Toggle button |
| Slider | Slider(SliderCfg) |
Min/max range picker |
| InputDate | InputDate(InputDateCfg) |
Date field with picker |
| ColorPicker | ColorPicker(ColorPickerCfg) |
RGBA color picker |
| Form | Form(FormCfg) |
Form with validation |
| ThemePicker | ThemePicker(ThemePickerCfg) |
Theme switcher |
| CommandPalette | CommandPalette(CommandPaletteCfg) |
Fuzzy-search command palette |
| Widget | Factory | Description |
|---|---|---|
| Text | Text(TextCfg) |
Styled text label |
| Badge | Badge(BadgeCfg) |
Notification badge |
| ProgressBar | ProgressBar(ProgressBarCfg) |
Determinate/indeterminate bar |
| Pulsar | Pulsar(PulsarCfg) |
Blinking cursor indicator |
| DrawCanvas | DrawCanvas(DrawCanvasCfg) |
Custom-draw surface (shapes, text, images, keyboard focus) |
| Image | Image(ImageCfg) |
Raster image view |
| Svg | Svg(SvgCfg) |
SVG vector image view |
| Markdown | w.Markdown(MarkdownCfg) |
Rendered Markdown |
| RTF | RTF(RtfCfg) |
Rendered RTF |
| Widget | Factory | Description |
|---|---|---|
| ListBox | ListBox(ListBoxCfg) |
Scrollable item list |
| Table | Table(TableCfg) |
Static data table |
| DataGrid | w.DataGrid(DataGridCfg) |
Virtualized grid with full CRUD |
| Tree | Tree(TreeCfg) |
Hierarchical tree view |
| DatePicker | DatePicker(DatePickerCfg) |
Calendar date picker |
| DatePickerRoller | DatePickerRoller(DatePickerRollerCfg) |
Drum-style date roller |
| Widget | Factory | Description |
|---|---|---|
| TabControl | Tabs(TabControlCfg) |
Tabbed panel |
| Breadcrumb | Breadcrumb(BreadcrumbCfg) |
Navigational breadcrumb trail |
| Menu | Menu(MenuCfg) |
Pull-down menu bar |
| Menubar | Menubar(w, MenubarCfg) |
Application menu bar |
| ContextMenu | ContextMenu(w, ContextMenuCfg) |
Right-click context menu |
| Sidebar | w.Sidebar(SidebarCfg) |
Collapsible side navigation |
| Widget | Factory | Description |
|---|---|---|
| Dialog | w.Dialog(DialogCfg) |
Modal dialog |
| Toast | w.Toast(ToastCfg) |
Transient notification |
| WithTooltip | WithTooltip(w, WithTooltipCfg) |
Hover tooltip wrapper |
┌─────────────────────────────────────────────────────────┐
│ Application Layer │
│ examples/ ── View fn(w *Window) *Layout │
│ gui.State[T](w) typed state slot │
└────────────────────────┬────────────────────────────────┘
│
┌────────────────────────▼────────────────────────────────┐
│ gui/ (core package) │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ Widgets │ │ State Mgmt │ │ Animation │ │
│ │ Button,Text │ │ StateMap │ │ Subsystem │ │
│ │ Container… │ │ per-window │ │ │ │
│ └──────┬───────┘ └──────┬───────┘ └───────┬───────┘ │
│ │ │ │ │
│ ┌──────▼─────────────────▼──────────────────▼───────┐ │
│ │ Layout Engine │ │
│ │ GenerateViewLayout() → Layout tree │ │
│ │ layoutArrange() — Fit/Fixed/Grow sizing │ │
│ │ renderLayout() → []RenderCmd │ │
│ └──────────────────────┬────────────────────────────┘ │
│ │ │
│ ┌──────────────────────▼────────────────────────────┐ │
│ │ Event Dispatch │ │
│ │ Mouse · Keyboard · Focus · Scroll │ │
│ └───────────────────────────────────────────────────┘ │
└────────────────────────┬────────────────────────────────┘
│
┌────────────────┼─────────────────┐
│ │ │
┌───────▼──────┐ ┌───────▼──────┐ ┌────────▼───────┐
│ TextMeasurer│ │ SvgParser │ │ NativePlatform │
│ (interface) │ │ (interface) │ │ (interface) │
└───────┬──────┘ └───────┬──────┘ └────────┬───────┘
│ │ │
┌───────▼────────────────▼─────────────────▼───────────┐
│ backend/sdl2/ │
│ Injects interfaces at startup · Window management │
├──────────────┬───────────────┬───────────────────────┤
│ backend/ │ backend/gl/ │ backend/filedialog/ │
│ metal/ │ OpenGL │ backend/printdialog/ │
│ Metal(macOS)│ │ │
├──────────────┼───────────────┼───────────────────────┤
│ backend/ │ backend/ios/ │ backend/spellcheck/ │
│ web/ │ Metal+UIKit │ backend/atspi/ │
│ WASM+Canvas │ (iOS) │ (Linux a11y) │
└──────────────┴───────────────┴───────────────────────┘
│
┌───────▼───────┐
│ go-glyph │
│ Text shaping │
│ rendering │
│ wrapping │
└───────────────┘
Key types: Layout (tree node), Shape (renderable), RenderCmd
(draw op), Window (top-level + state slot)
Tests run headlessly via the gui/backend/test no-op backend — no display
server required.
# Run all tests
go test ./...
# Run a specific test
go test ./gui/... -run TestFoo
# Static analysis
go vet ./...
# Full lint suite (govet, staticcheck, errcheck, gosimple, unused,
# gofmt, goimports, revive)
golangci-lint run ./gui/...
# Build all packages
go build ./...- Install Go 1.26+ and SDL2 development libraries (see Installation).
- Clone the repo.
- Run tests and lint:
go test ./...
go vet ./...
golangci-lint run ./gui/...- Open a pull request with a clear description of the change.




