iced
Elm-inspired reactive GUIs in Rust - explicit messages, immutable updates, and native widgets.
Recipe
use iced::widget::{button, column, text};
use iced::Element;
pub fn update(count: &mut i32, message: Message) {
match message {
Message::Increment => *count += 1,
}
}
pub fn view(count: &i32) -> Element<Message> {
column![
text(format!("{count}")),
button("+").on_press(Message::Increment),
]
.into()
}When to reach for this:
- Consumer desktop apps without HTML
- Teams liking Redux/Elm unidirectional data flow
- Cross-platform apps needing consistent custom styling
- Tutorial-friendly architecture for junior Rust devs
Working Example
use iced::widget::{button, column, text, Column};
use iced::{application, Element};
#[derive(Debug, Clone)]
enum Message {
Increment,
Decrement,
}
struct Counter {
value: i32,
}
impl Counter {
fn new() -> Self {
Self { value: 0 }
}
fn update(&mut self, message: Message) {
match message {
Message::Increment => self.value += 1,
Message::Decrement => self.value -= 1,
}
}
fn view(&self) -> Element<Message> {
column![
text(format!("{}", self.value)),
button("+").on_press(Message::Increment),
button("-").on_press(Message::Decrement),
]
.into()
}
}
fn main() -> iced::Result {
application("Counter", Counter::update, Counter::view)
.run_with(Counter::new)
}What this demonstrates:
applicationhelper wiring update/view- Separate increment/decrement messages
- Column layout composing widgets
Deep Dive
How It Works
- Runtime receives OS events, converts to
Message. updatemutates model;viewrebuildsElementtree.- Diffing renderer minimizes GPU work (wgpu backend).
Subscriptionstreams external events (time, keyboard).
Architecture Pieces
| Piece | Role |
|---|---|
| Model | App state struct |
| Message | Event enum |
| update | State transition |
| view | UI function |
Gotchas
- Large
viewfunctions - hard to maintain. Fix: split into submodules returningElement. - Forgetting
subscription- timers never fire. Fix: wireapplication::subscriptionwhen using async sources. - Clone on every message - heavy models hurt. Fix: use
Arcfor read-only chunks in model. - Platform font rendering - verify CJK on all targets. Fix: custom font loading in settings.
- ** iced 0.13 API changes** - pin book/tutorial version to crate version.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| egui | Immediate-mode tools | Prefer strict MVU testing |
| Tauri | Web UI team | No HTML desired |
| Slint | Designer-driven DSL | Rust-only widget code preferred |
| Qt via CXX | Mature native controls | Pure Rust dependency goal |
FAQs
How to test update logic?
Pure update functions unit test without running window - pass messages, assert model.
Async HTTP in iced?
Use Command/Task patterns (version-dependent) to perform HTTP and map result to Message.
Custom styling?
Theme customization and style methods on widgets for brand colors.
Multiple windows?
Supported in advanced APIs - start single-window until needed.
wgpu vs tiny-skia?
wgpu for GPU acceleration; software renderer for CI and headless screenshots.
Navigation stacks?
Model routes as enum; view matches route to page functions.
Forms validation?
Store field errors in model; display near widgets in view.
Packaging?
Packaging & Distribution for release binaries.
State at scale?
State & Architecture for MVU + layers.
vs egui?
iced retained/diff; egui immediate - see egui.
Related
- GUI Basics - iced counter intro
- State & Architecture - scaling MVU
- egui - immediate-mode alternative
- Packaging & Distribution - releases
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.