An Elm-architecture GUI framework for Odin with immediate-mode-level performance. Pure-functional state, rebuild-every-frame rendering, and a modern dark/light aesthetic out of the box.
examples/00_gallery — the shipped widget set in both themes.
Skald reads like iced or Elm at the API level, and its rebuild-every-frame
model puts it in the immediate-mode performance tier rather than the
retained-tree one. See docs/benchmarks.md for
actual numbers.
Pure-Odin text backend (runa) is now the default. Skald ships
runaas the default text engine, withvendor:fontstashretained as a fallback (build with-define:SKALD_RUNA=falseto opt out). Runa brings real OpenType shaping (ligatures, GPOS kerning, contextual alternates), COLRv0 + COLRv1 colour emoji (with linear / radial / sweep gradients), and subpixel-x positioning — and it's faster than fontstash on every benched workload (gallery 2.2× faster).
- Elm architecture —
init/update/viewwith a pureMsgunion. No imperative widget handles, no signal wiring; your app'sStatestruct is the source of truth for app-level data. - Immediate-mode performance — the view tree is rebuilt every frame from the app's state, so there's no retained widget graph to diff or keep in sync. Frame times under 1 ms on most apps on a modern desktop GPU (Linux); capped by display refresh on macOS.
- GPU rendering — pure-Odin Vulkan 1.3 backend (
vendor:vulkan), one pipeline for rects + text + images. - Two text backends — the pure-Odin
runaengine ships vendored atskald/third_party/runa/and is the default since 1.0: full OpenType shaping (ligatures, GPOS kerning), COLRv0 + COLRv1 colour emoji (with linear / radial / sweep gradients), and subpixel-x positioning. Runa is faster than fontstash on every benched workload (gallery 2.2× faster).vendor:fontstashis retained as a smaller, no-shaping fallback — opt back in with-define:SKALD_RUNA=falseif you need it. - Async — the
Command(Msg)effect system runs file I/O, native dialogs, and delays throughcore:nbioon the main thread; completions round-trip back as regularMsgvalues. For sync libraries that aren't nbio-shaped (postgres, sqlite, big-file parsers),cmd_threadruns the blocking call on a worker thread and delivers the return value as a Msg. The UI never freezes;viewandupdatealways run single-threaded. - Real widget set — buttons, checkboxes, sliders, selects, text inputs (single + multi-line, with undo/clipboard/selection), tables, virtual lists, tabs, menus, dialogs, image, split panes, toasts, drag-and-drop, context menus, command palette, tree, date / time / color pickers.
- Themable — dark + light themes ship in-tree (GitHub Primer / Radix
tuned); custom themes are a plain struct. Ships with i18n
Labels, font-fallback chaining (font_add_fallback), and asystem_theme()probe plus anon_system_theme_changecallback so apps can follow the OS light/dark setting if they want. - Window state hooks —
initial_window_stateto seed the window on launch,on_window_state_changeto hear about user resize / move / maximize. Your app decides where/how to persist the state (JSON on disk, embedded in asettings.db, wherever) — Skald just reports the current geometry as a plain struct. Cookbook has the pattern. - Multi-window —
cmd_open_window/cmd_close_windowspawn and tear down extra OS windows (popovers, notification bubbles, panels, HUDs). Each gets its own Vulkan swapchain, input, and widget store; the app'sviewis called per window withctx.windowset so one proc can render all of them. Device, pipeline, fonts, and images stay shared — only the swapchain and per-frame plumbing are per-window. - Keyboard-first — Tab ring, shift-Tab, focus trap inside modals,
focus restoration on dialog/palette close,
widget_tab_indexfor explicit ordering, globalshortcut()registration. - Cross-platform — Linux (primary), Windows, macOS.
Skald has one external C dependency: SDL3, for cross-platform
windowing, input, clipboard, file dialogs, and HiDPI / multi-monitor
handling. We use it via Odin's vendor:sdl3 bindings — those are
Odin procs that map to SDL3's C ABI, but the runtime that actually
does the work is the C library (libSDL3.so / SDL3.dll /
libSDL3.dylib), so a Skald binary links against and loads compiled
C code at startup. vendor:sdl3 is bindings, not a re-implementation.
Everything else — Vulkan renderer, OpenType shaping (via the vendored
pure-Odin runa engine), layout, widgets, atlas, theming, async — is
pure Odin top-to-bottom.
Why SDL3 specifically: cross-platform windowing and input is a vast edge-case surface (Wayland ↔ X11 transitions, IME, pen tablets, fractional scaling, multi-monitor refresh, macOS Spaces, Windows raw input, fullscreen modes, drag-and-drop) and SDL3 is the most battle-tested option that handles all of it. Rolling our own per-OS platform layer (X11+Wayland / Win32 / Cocoa) would be months of work and a long tail of edge cases to re-discover. A pure-Odin platform layer is on the table post-1.0 if uptake justifies the maintenance cost; for v1 the trade-off doesn't pay back.
Prerequisites vary by platform. See PLATFORMS.md for
the exact commands; the short version:
- Linux — install SDL3 (
sudo apt install libsdl3-0or equivalent) and a Vulkan loader (libvulkan1on most distros). On Ubuntu 24.04 LTS, SDL3 isn't yet packaged —PLATFORMS.mdhas the build-from- source recipe. First-time Odin install also needs the stb static libs built once:make -C $ODIN_ROOT/vendor/stb/src. - Windows — Odin's
vendor:sdl3bundlesSDL3.dllandbuild.batcopies it next to the exe automatically. Vulkan loader ships with recent GPU drivers or the LunarG Vulkan SDK. - macOS —
brew install sdl3plus the LunarG Vulkan SDK (installs MoltenVK + loader). Source~/VulkanSDK/*/setup-env.shin your shell, or run the SDK's system-install script.
git clone <your-fork> skald && cd skald
./build.sh 07_counter run # build + run the counter exampleOn Windows:
build.bat 07_counter runpackage hello
import "core:fmt"
import "gui:skald"
State :: struct { count: int }
Msg :: enum { Inc, Dec }
init :: proc() -> State { return {} }
update :: proc(s: State, m: Msg) -> (State, skald.Command(Msg)) {
switch m {
case .Inc: return {count = s.count + 1}, {}
case .Dec: return {count = s.count - 1}, {}
}
return s, {}
}
view :: proc(s: State, ctx: ^skald.Ctx(Msg)) -> skald.View {
th := ctx.theme
return skald.col(
skald.text(fmt.tprintf("Count: %d", s.count),
th.color.fg, th.font.size_display),
skald.spacer(th.spacing.xl),
skald.row(
skald.button(ctx, "−", Msg.Dec, width = 64),
skald.button(ctx, "+", Msg.Inc, width = 64,
bg = th.color.primary, fg = th.color.on_primary),
spacing = th.spacing.md,
),
padding = th.spacing.xl,
main_align = .Center,
cross_align = .Center,
)
}
main :: proc() {
skald.run(skald.App(State, Msg){
title = "Hello",
size = {640, 400},
theme = skald.theme_dark(),
init = init,
update = update,
view = view,
})
}See examples/07_counter for the annotated version.
docs/getting_started.md— install, run an example, understand what the code is doing.docs/guide.md— build a small app from scratch, picking up widgets and patterns as you go.docs/cookbook.md— short recipes for the things you'll keep reaching for: forms, dialogs, shortcuts, theming, async, persistence.docs/gotchas.md— the foot-guns worth knowing about before you hit them.docs/widgets.md— per-widget reference.docs/widget_choice.md— short decision tree for "which widget should I reach for here?"docs/examples.md— annotated index of every example, grouped by concept.docs/distributing.md— shipping your Skald app to end users: bundling SDL3 on Linux, app bundles on macOS, what each platform's user actually needs installed.docs/architecture.md— how the framework fits together (for people extending Skald, not using it).docs/benchmarks.md— frame-time and memory numbers with the env vars to reproduce them.- Per-function reference — run
odin doc ./skaldfrom the project root.
skald/ the framework package (pure Odin; imports vendor:vulkan/sdl3)
examples/ NN_topic/main.odin — runnable demos (52 of them)
docs/ tutorial, cookbook, widget reference, benchmarks
build.sh/.bat build one example into ./build/ (use RELEASE=1 to strip -debug)
bench.sh run the canonical bench suite
CHANGELOG.md per-release notes
LICENSE zlib
NOTICE bundled-Inter + third-party acknowledgements
PLATFORMS.md per-platform setup + known quirks
| Platform | Status |
|---|---|
| Linux | Primary target (X11 + Wayland via SDL3) |
| Windows | Supported, SDL3 DLL auto-copied by build |
| macOS | Supported via MoltenVK |
A skáld (Old Norse) was a court poet — composer and reciter of ornate stanzaic verse in intricate alliterative meters, built up from individual stanzas into longer praise poems called drápur. The metaphor fits: a Skald app is one view composed from small widgets, each shaped with care. It's also a thematic nod to Odin itself, which the framework is built on.
Skald is licensed under the zlib license — see LICENSE.
Permissive: commercial use, modification, and redistribution are all
fine, with three light restrictions (don't misrepresent origin, mark
altered versions, keep the notice). Matches Odin's own license for
zero ecosystem friction.
Bundled-asset and third-party acknowledgements live in NOTICE
(separated from LICENSE so GitHub's auto-detector recognises the
zlib header cleanly).
The bundled Inter typeface — InterVariable plus the static-weight
Inter-Bold, Inter-Italic, and Inter-BoldItalic faces used by
rich_text for emphasis — ships under the SIL Open Font License
1.1; the full text travels alongside the fonts at
skald/assets/InterVariable-OFL.txt.
Skald stands on good shoulders. Credit and thanks to:
- Odin (Ginger Bill) — the amazing language itself.
- SDL3 (Sam Lantinga and the SDL contributors) — windowing, input, clipboard, file dialogs, drag-and-drop.
- Inter (The Inter Project Authors) —
the bundled UI typeface. InterVariable is the default; the
static-weight Bold / Italic / BoldItalic faces ship alongside it
for
rich_text's emphasis spans. - runa (Lee Fry + contributors) — pure-Odin text engine vendored
at
skald/third_party/runa/. zlib licence. Default text backend since 1.0; full OpenType shaping (GSUB / GPOS), Arabic / Indic / SEA shaping, RTL + bidi, and COLRv0 / COLRv1 colour emoji. - fontstash
(Mikko Mononen) — glyph atlas management, shipped via
vendor:fontstash. Legacy text backend retained as a fallback (-define:SKALD_RUNA=false). - stb (Sean T. Barrett) —
stb_truetype(used inside fontstash) andstb_image(PNG loading). - Vulkan — specification by the Khronos Group; on macOS, Vulkan calls are translated to Metal by MoltenVK (originally by Brenwill Workshop Ltd., now maintained under Khronos).
If you build something nice with Skald, no obligation — but I'd love to see it.
Small fixes: send a PR. Larger changes: open an issue first. The
open work areas the project would most benefit from — particularly
complex-script shaping (Arabic / Hebrew / Devanagari / Thai),
where the right contributor is a native speaker of the target
language — are written up in CONTRIBUTING.md.