Building web frontends in Swift with ElementaryUI
I tried ElementaryUI, a framework for writing web frontends in Swift using SwiftWasm. It has a declarative style similar to SwiftUI.
- https://forums.swift.org/t/introducing-elementaryui-swift-in-the-browser/83952
- https://elementary.codes/
Elementary itself has been around for a while as an HTML rendering library, but that was mainly for generating HTML on the backend. Now it’s grown into something like SwiftUI or React — you describe state declaratively and build dynamic UIs. Compile it to .wasm and it runs in the browser.
Here’s how it differs from SwiftUI (it’s not trying to be SwiftUI-compatible though):
- Views are built with a DSL using types named after HTML tags
- SwiftUI-style annotations like
@State,@Bindingare created with macros (so you can see the expanded code)- However, it doesn’t use the Observation framework to support Embedded Swift — it has a custom
@Reactivemacro instead
- However, it doesn’t use the Observation framework to support Embedded Swift — it has a custom
- No style modifiers, so you need CSS
- CSS frameworks like Tailwind are helpful
- SwiftUI-style animations are supported
- You define views in the
bodyproperty of a struct, so it feels similar - Auto-builds on save and hot-reloads localhost — quite pleasant
- When using the Vite plugin ElementaryUI provides
- Incremental builds make it reasonably fast
Sample code
From the official site:
@View
struct LoveCounter {
@State var count = 1
var body: some View {
p { String(repeating: "❤️", count: count) }
if count < 10 {
button { "More Love" }
.onClick { count += 1 }
} else {
p { "Enough love for you!" }
button { "Less Love" }
.onClick { count = 1 }
}
}
}
This creates a dynamic UI. You’re essentially writing HTML, but the feel is quite SwiftUI-like.
Todo app
I built the classic todo app. No persistence — just keeps the list in memory.
Loading tweet...
Tweet by @ainame
- Repository: https://github.com/ainame/elementary-ui-todo-app
- Demo: https://ainame.github.io/elementary-ui-todo-app
There’s no standard architecture, so I went with MVVM, common in mobile apps. Adding @Reactive makes it behave like @Observable, so writing a ViewModel is easy. You could also just use @State in the view for a simpler setup.
import Reactivity
@Reactive
class TodoViewModel {
private(set) var items: [TodoItem] = []
private var lastId = 0
func onClickAddNewButton() {
lastId += 1
items.append(
TodoItem(
id: lastId,
title: "Title",
description: "Description",
deadline: "2026-01-15"
)
)
}
func onClickDeleteButton(at index: Int) {
items.remove(at: index)
}
func onClickDoneButton(with item: TodoItem, at index: Int) {
items[index] = item
}
}
Adding @View turns body into an @HTMLBuilder result builder, enabling the DSL. It’s mostly HTML, but you can use if statements and ForEach with key tracking for animated lists.
import ElementaryUI
@View
struct ContentView {
let viewModel = TodoViewModel()
var body: some View {
div(.class("max-w-3xl mx-auto p-8")) {
button(
.class(
"bg-gradient-to-r from-blue-500 to-blue-600 hover:from-blue-600 hover:to-blue-700 text-white font-bold px-6 py-3 rounded-lg shadow-lg hover:shadow-xl transition-all duration-300 mb-8 w-full"
)
) {
"➕ Add New Todo"
}
.onClick {
viewModel.onClickAddNewButton()
}
div(.class("space-y-4")) {
ForEach(viewModel.items.enumerated(), key: { String($0.element.id) }) { item in
TodoItemView(
item: item.element,
onClickDeleteButton: { viewModel.onClickDeleteButton(at: item.offset) },
onClickDoneButton: { viewModel.onClickDoneButton(with: $0, at: item.offset) },
)
}
}
}
.animateContainerLayout()
.animation(.smooth, value: viewModel.items.count)
}
}
Wasm file size
The ElementaryUI template comes with Vite pre-configured. Run pnpm run build and you get index.html, JS, CSS, and a Wasm file ready to deploy. I just uploaded these to GitHub Pages for the demo.
The Wasm size matters. With useEmbeddedSDK = true and useWasmOpt, I got it down to 177 kB (gzipped). Without those, even a simple todo app is around 2 MB.
$ pnpm run build
...
dist/index.html 0.36 kB │ gzip: 0.25 kB
dist/assets/WebApp-CcEiOZXG.wasm 449.76 kB │ gzip: 177.88 kB
dist/assets/index-DjD_SlHc.css 13.71 kB │ gzip: 3.30 kB
dist/assets/index-BlOi47Zf.js 32.93 kB │ gzip: 8.50 kB
useEmbeddedSDK = false
$ pnpm run build
...
dist/index.html 0.36 kB │ gzip: 0.25 kB
dist/assets/WebApp-B03SLlGi.wasm 5,127.45 kB │ gzip: 1,968.64 kB
dist/assets/index-DBVn_mL9.css 14.01 kB │ gzip: 3.36 kB
dist/assets/index-oR8VjpfT.js 32.93 kB │ gzip: 8.50 kB
useEmbeddedSDK = false & import Foundation
$ pnpm run build
...
dist/index.html 0.36 kB │ gzip: 0.25 kB
dist/assets/WebApp-qs0ldgOg.wasm 5,135.93 kB │ gzip: 1,971.95 kB
dist/assets/index-DBVn_mL9.css 14.01 kB │ gzip: 3.36 kB
dist/assets/index-CNLES2eP.js 32.93 kB │ gzip: 8.50 kB
With Foundation imported, you get Date and other types. When useEmbeddedSDK = false, Foundation barely changes the file size. So decide on Embedded SDK based on whether you need Foundation.
With Embedded Swift, Foundation isn’t available yet. You’ll need alternatives for URLSession, Date, Codable, etc. I hope clearer guidelines emerge over time.
Thoughts
ElementaryUI is still at v0.1.1 — very early. Documentation isn’t comprehensive, but I was surprised at how well the demo worked. I haven’t tried building a proper SPA yet, but it looks promising if that gets sorted. Questions remain: how do you handle HTTP clients, URLs, cookies?
SwiftWasm has JavaScriptKit for calling JavaScript from Swift. That could cover the basics, but ElementaryUI doesn’t handle everything for you yet.
Compared to TypeScript and React, there’s no clear win for Swift beginners. It would be good to evaluate SwiftWasm’s performance for compute-heavy tasks, or the benefit of a unified Swift codebase for backend and frontend.
For experienced Swift developers though, writing in a familiar language is nice. You avoid dealing with JS tooling. The template uses Vite, so you need Node, but you could skip that if you set everything up yourself.
Other languages target Wasm too. Here’s how I’d compare:
- Rust — Swift is easier for typical frontend code
- Kotlin
- Compose Multiplatform can deploy to web via Wasm, which is big
- But can you truly share all code? I’m not sure
- The demo page feels sluggish: https://zal.im/wasm/jetsnack/
- Gradle has quirks and a strong JetBrains IDE dependency
- Swift LSP has improved a lot, so VSCode works fine now. Import management is way easier too
- Compose Multiplatform can deploy to web via Wasm, which is big
MoonBit targets WebAssembly — mizchi has been talking about it — but it’s still young as a language.
(Edit): The official site has a v1 roadmap. The things I was curious about will be addressed before v1.
Here are a few high-level topics that are still ahead
- Support for creating web components (custom elements)
- Fill in missing gaps for value bindings and event handlers
- Client-side router implementation (for route-based SPAs)
- Mechanism for fetching JSON data (embedded compatible)
- Web Storage APIs (local/session storage, embedded compatible)
- Complete transform and filter effects and built-in transitions
- Swift-native layout and style module (idiomatic APIs for CSS)
- Support more CSS properties in FLIP animations
- Investigate how to further reduce wasm binary size
- Server-side rendering and client-side hydration
- “Meta-Framework” features
https://elementary.codes/guide/introduction.html
References
- Thread on networking challenges in Embedded Swift: https://forums.swift.org/t/what-is-swift-embeddeds-vision-for-networking-filesystem-syscalls-etc/
- Discussion on future serialisation APIs for Embedded Swift: https://forums.swift.org/t/the-future-of-serialization-deserialization-apis/
- Discussion on filtering ICU data: https://forums.swift.org/t/proposal-use-filtered-icu-data-as-the-default-in-swift-foundation-icu/
- Moonbit: https://zenn.dev/mizchi/articles/introduce-moonbit