Skald is an Elm-architecture GUI framework for Odin with immediate-mode-level
performance. You write four procs — init, update, view, main — and
Skald turns them into a window that responds to clicks and keystrokes.
State lives in your app, messages flow through a pure update, the view
tree is rebuilt from scratch every frame. That's the whole deal.
You'll need:
- Odin on your PATH.
- SDL3 —
apt install libsdl3-0on Debian 13+ / Devuan / Fedora 40+ / Arch. Ubuntu 24.04 LTS doesn't ship SDL3 yet — seePLATFORMS.mdfor a from-source recipe. On Windows, Odin'svendor:sdl3shipsSDL3.dllandbuild.batcopies it next to the exe automatically. On macOS,brew install sdl3. - Vulkan loader + driver —
libvulkan1+mesa-vulkan-driverson Linux, ships with recent GPU drivers on Windows, LunarG Vulkan SDK (which includes MoltenVK) on macOS. - First-time on Linux only:
make -C $ODIN_ROOT/vendor/stb/srconce per machine. Skald's bundled fontstash links againststb_truetype.awhich Odin ships as C source. (Skipped if you build with the pure-Odin runa backend instead — see below.)
Then:
git clone <repo> skald && cd skald
./build.sh 07_counter runSkald ships with two text engines:
- runa (default since 1.0): a pure-Odin text engine vendored at
skald/third_party/runa/. Adds OpenType shaping (ligatures, kerning), COLRv0 + COLRv1 colour emoji (with linear / radial / sweep gradients), Arabic / Indic / SEA shaping with RTL + bidi, and is faster than fontstash. No C dependency, so thestbstep above is skipped under the default backend. - fontstash (legacy fallback): the long-shipped path. Requires
the one-time
stb_truetype.abuild above. Opt back into it withSKALD_RUNA=0 ./build.sh 07_counter run(or-define:SKALD_RUNA=false) if you need it — most apps don't.
A window opens with a − / Reset / + counter. Click the buttons, the number changes. If that works your toolchain is healthy and you can stop reading this section.
On Windows, open "x64 Native Tools Command Prompt" before build.bat
— Odin links through link.exe.
Open examples/07_counter/main.odin. It's around 70 lines and every Skald
app follows the same shape:
State :: struct { count: int }
Msg :: enum { Inc, Dec, Reset }
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}, {}
case .Reset: return {count = 0}, {}
}
return s, {}
}
view :: proc(s: State, ctx: ^skald.Ctx(Msg)) -> skald.View {
// ...build a tree of buttons and text out of `s`...
}State is your data. Msg is every action the user can take. update
answers "given the old state and a message, what's the new state?"
view answers "given the state, what should be on screen right now?"
Skald calls view every frame and draws what it returns. When you
click a button, Skald takes whatever Msg that button carries, hands
it to update, and renders the new state on the next frame. That's
the whole loop.
No widget handles. No event listeners. No observable chains. You describe the UI you want, Skald makes it be there.
The ./build.sh 07_counter run command above only works from inside
this repo. For your own project, Skald is an Odin package that you
reference via a collection — Odin's name for a named root from which
import paths resolve.
Three common patterns; pick whichever fits your workflow:
- Sibling clone —
git clone <skald-url>next to your project folder. Simple, no submodule juggling. Good for solo work. - Git submodule —
git submodule add <skald-url> vendor/skaldinside your repo. Pins a specific Skald commit; collaborators get the exact same version on clone. - Vendored copy — drop the
skald/folder straight into your repo. Works offline, no external dependency, but updates are manual.
Whichever you pick, the important thing is that the path to the
directory that contains skald/ is stable. That directory is what
you'll hand to the compiler as a collection root.
Odin resolves import "foo:bar" by looking up the collection named
foo and reading bar/ beneath it. Skald's examples use gui as
the collection name, so import "gui:skald" means "find the
collection called gui, then look for the skald/ package under
it."
Your main.odin — a complete, runnable "Hello, Skald!" you can
paste verbatim:
package my_app
import "gui:skald"
State :: struct {}
Msg :: struct {}
init :: proc() -> State { return {} }
update :: proc(s: State, m: Msg) -> (State, skald.Command(Msg)) { return s, {} }
view :: proc(s: State, ctx: ^skald.Ctx(Msg)) -> skald.View {
th := ctx.theme
return skald.text("Hello, Skald!", th.color.fg, th.font.size_lg)
}
main :: proc() {
skald.run(skald.App(State, Msg){
title = "Hello",
size = {640, 480},
theme = skald.theme_dark(),
init = init,
update = update,
view = view,
})
}The stubs (State/Msg/init/update/view) are empty on purpose —
this program does nothing, which is exactly the point for confirming
your build is set up. guide.md walks through filling
them in to build a real to-do app.
Your build command tells Odin what gui points to:
odin build . -collection:gui=/path/to/skald-parent -out:build/my_app/path/to/skald-parent is the directory that contains the skald/
folder — not the skald/ folder itself. For the sibling-clone
layout where your project and the Skald clone are peers, that's
../skald-repo-name.
Save this next to your main.odin and chmod +x it:
#!/usr/bin/env bash
set -euo pipefail
cd "$(dirname "$0")"
mkdir -p build
# Adjust this path so it points at the directory containing the
# `skald/` folder. For a sibling git clone called `skald`, it's `../skald`.
SKALD_ROOT="../skald"
odin build . \
-collection:gui="${SKALD_ROOT}" \
-debug \
-out:build/my_app
if [[ "${1:-}" == "run" ]]; then
exec ./build/my_app
fi./build.sh compiles, ./build.sh run compiles and runs. Drop -debug
(or replace with -o:speed) for release builds — the F12 debug
inspector is when ODIN_DEBUG-gated, so release binaries strip it
out automatically.
build.bat:
@echo off
cd /d "%~dp0"
if not exist build mkdir build
set SKALD_ROOT=..\skald
odin build . -collection:gui=%SKALD_ROOT% -debug -out:build\my_app.exe
if errorlevel 1 exit /b 1
if "%~1"=="run" build\my_app.exeRun it from "x64 Native Tools Command Prompt" so link.exe is on
PATH. Odin's vendor:sdl3 bundles SDL3.dll and the build copies
it next to your .exe automatically.
guide.mdbuilds a small to-do app from scratch so you see where each piece comes in.cookbook.mdis the "how do I?" grab-bag — short recipes for forms, dialogs, shortcuts, theming, async, etc.gotchas.mdlists the handful of things that will trip you up if you haven't heard about them yet.widgets.mdis the widget menu — signatures, what they do, when to reach for each one.widget_choice.mdis a short decision tree for "which widget should I reach for here?" — useful when you're not sure whether you wantselectvscomboboxvsradio_group.distributing.mdcovers shipping a Skald app to end users — bundling SDL3 next to the binary on Linux, app bundles on macOS, what each platform's user actually needs.odin doc ./skalddumps every public proc if you want the raw reference.