This documents how to build, run, and develop SwiftOpenUI examples for the browser via WebAssembly.
| Component | Version | Notes |
|---|---|---|
| Swift toolchain | 6.2.4 (open-source, via swiftly) | Xcode's Swift lacks the Wasm backend |
| Wasm SDK | swift-6.2.4-RELEASE_wasm | Install via ./configure or manually |
| Node.js | 18+ | For Vite dev server and screenshot tooling |
| Google Chrome | Any | For Puppeteer screenshots (optional) |
# 1. Setup toolchain + Wasm SDK (one-time, macOS only)
# For Linux, install the open-source Swift toolchain and Wasm SDK manually
# (see docs/guides/getting-started.md for platform-specific setup)
./configure
# 2. Build all Wasm examples
source ~/.swiftly/env.sh
./web/build-wasm.sh
# 3. Install Node dependencies
cd web && npm install
# 4. Start dev server
npx vite
# Open http://localhost:3000Swift source → SwiftWasm compiler → .wasm binary
↓
PackageToJS plugin (SPM)
↓
JS module (index.js, runtime.js, instantiate.js)
↓
HTML page + import map → Browser
swift package --swift-sdk ... js --product <name>compiles Swift to.wasmand generates a JS module package at.build/plugins/PackageToJS/outputs/Package/build-wasm.shcopies each product's output toweb/examples/<name>/and creates an HTML page- Vite serves the HTML pages with proper module resolution
SwiftOpenUI's WebBackend uses JavaScriptKit to bridge Swift and the browser DOM:
// Swift side (WebRenderer.swift)
let div = document.createElement("div")
div.style = "display: flex; flex-direction: column;"JavaScriptKit translates these calls to JavaScript DOM operations at runtime through the Wasm↔JS bridge.
WebAssembly programs compiled for WASI need a WASI implementation in the browser. The @bjorn3/browser_wasi_shim package provides this. Each example HTML page includes an import map to resolve this dependency:
<script type="importmap">
{
"imports": {
"@bjorn3/browser_wasi_shim": "https://esm.sh/@bjorn3/browser_wasi_shim@0.3.0"
}
}
</script>web/
├── index.html ← Example index page (links to all examples)
├── package.json ← Node dependencies (Vite, Puppeteer, browser_wasi_shim)
├── vite.config.js ← Vite dev server config
├── build-wasm.sh ← Builds all Wasm examples + generates HTML pages
├── screenshot.mjs ← Puppeteer screenshot capture script
├── .gitignore ← Ignores node_modules/, dist/, examples/
└── examples/ ← Generated (not checked in)
├── HelloWorld/ ← PackageToJS output (JS + .wasm)
├── HelloWorld.html ← HTML page that loads the example
├── TextStyles/
├── TextStyles.html
└── ...
./web/build-wasm.shThis builds each product with swift package --swift-sdk ... js --product <name>, copies the output to web/examples/, and creates HTML pages.
swift package --swift-sdk swift-6.2.4-RELEASE_wasm js --product HelloWorldOutput: .build/plugins/PackageToJS/outputs/Package/
- Add the example to
Examples/<name>/main.swiftwith the standard#ifboilerplate - Add an executable target in
Package.swift - Add the product name to
PRODUCTSarray inweb/build-wasm.sh - Add a link in
web/index.html - Add an entry in
web/screenshot.mjs - Run
./web/build-wasm.sh
cd web
npm install # first time only
npx vite # starts at http://localhost:3000The index page at localhost:3000 links to all examples. Click one to open it.
Vite serves the generated HTML pages which include inline import maps (generated by build-wasm.sh) to resolve @bjorn3/browser_wasi_shim from esm.sh. Vite handles correct MIME types for .wasm files.
After changing Swift source code:
./web/build-wasm.sh # rebuild all
# or rebuild one:
swift package --swift-sdk swift-6.2.4-RELEASE_wasm js --product HelloWorld
cp -r .build/plugins/PackageToJS/outputs/Package web/examples/HelloWorldThen refresh the browser. (No hot reload for Wasm — you must rebuild and refresh.)
If you switch between toolchains (e.g. 6.2.4 ↔ 6.3 snapshot), clean first:
swift package cleanThe WebRenderer maps SwiftOpenUI views to HTML elements via CSS:
| SwiftOpenUI | HTML/CSS |
|---|---|
| Text("hello") | <span>hello</span> |
| Button("tap") { } | <button onclick=...>tap</button> |
| VStack { } | <div style="display:flex; flex-direction:column"> |
| HStack { } | <div style="display:flex; flex-direction:row"> |
| ZStack { } | <div style="display:grid; place-items:center"> (children use grid-area: 1/1) |
| Spacer() | <div style="flex:1"> |
| Divider() | <hr> |
| Color.blue | <div style="background-color:rgba(0,0,255,1)"> |
| .padding(8) | <div style="padding:8px"> wrapping child |
| .frame(width:100) | <div style="width:100px"> wrapping child |
| .foregroundColor(.red) | <div style="color:rgba(255,0,0,1)"> wrapping child |
| .background(.green) | <div style="background-color:..."> wrapping child |
| .font(.title) | <div style="font-size:28px; font-weight:bold"> wrapping child |
| .border(.red) | <div style="border:1px solid rgba(255,0,0,1)"> wrapping child |
When @State changes, WebViewHost schedules a rebuild via requestAnimationFrame. On rebuild:
- Container's
innerHTMLis cleared - New DOM elements are created from the updated view tree
- Appended to the container in one pass
This is a full re-render, not a diff — acceptable for the current scope.
Automated via Puppeteer (headless Chrome):
# Start Vite in one terminal
cd web && npx vite
# Run screenshots in another terminal
cd web && node screenshot.mjsSaves to screenshots/web/. Uses 2x device scale factor and fullPage: true.
Requires Google Chrome installed at /Applications/Google Chrome.app.
- No hot reload — Wasm must be rebuilt and the browser refreshed after Swift changes
- Debug builds are large — ~59MB per example (includes full Swift runtime). Release builds with
wasm-optwould be much smaller. - Spacer doesn't expand — CSS
flex:1doesn't work the same as native flex layout without a constrained parent height - Full re-render on state change — no DOM diffing yet; entire view tree is rebuilt. Fine for small examples, would need optimization for larger apps.
- Import maps for standalone serving — when serving without Vite/Node (e.g.
npx serve), each HTML page needs an import map for@bjorn3/browser_wasi_shim. Thebuild-wasm.shscript generates these automatically. - macOS-only toolchain — Wasm cross-compilation requires the open-source Swift toolchain, which
./configureinstalls on macOS. Linux hosts can also cross-compile but are not covered by the setup script.