Iced
Cross-platform GUI library inspired by Elm architecture. Based on type safety and functional programming principles, achieving predictable state management. Supports beautiful animations and custom widgets.
GitHub Overview
iced-rs/iced
A cross-platform GUI library for Rust, inspired by Elm
Topics
Star History
Framework
Iced
Overview
Iced is a cross-platform GUI library for Rust, inspired by Elm. Based on the principles of Elm architecture, it enables type-safe and predictable GUI application development, bringing functional programming paradigms to GUI development as an innovative framework.
Details
Iced is a GUI framework that ports the Elm language architecture to Rust, continuing its steady evolution as of 2025. By adopting the Model-View-Update (MVU) pattern, it achieves clarity in state management and type safety, liberating developers from traditional callback hell.
Iced's architecture consists of three elements: application state (Model), a function that renders the state (View), and pure functions that update the state (Update). This pattern makes application behavior predictable, significantly simplifying debugging and testing.
Technically, Iced provides multiple rendering backends, allowing selection of optimal rendering approaches based on requirements, including wgpu (WebGPU), glow (OpenGL), and TinySkia. WebAssembly support enables building both native and web applications from the same codebase, achieving true cross-platform development.
Pros and Cons
Pros
- Elm Architecture: Predictable and type-safe state management
- Functional Paradigm: Elimination of side effects and clear data flow
- True Cross-Platform: Works on both native and web platforms
- Flexible Rendering: Multiple backend support
- Excellent Testability: Easy unit testing with pure functions
- Rich Widgets: Comprehensive components including buttons, text, sliders
- Async Support: Integration with futures/async patterns
Cons
- Learning Curve: Understanding of Elm architecture required
- Performance Constraints: Heavier processing than immediate mode
- Maturity: More limited features compared to other GUI libraries
- Community: Relatively small developer community
- Documentation: Limited Japanese resources
- Complex State Management: Increased boilerplate in large applications
Key Links
- Iced GitHub Repository
- Iced Documentation
- Iced Official Examples
- Iced Book
- Iced Online Demo
- Iced Community
Code Examples
Hello World Counter App
use iced::widget::{button, column, text, Column};
use iced::{Element, Sandbox, Settings};
#[derive(Default)]
struct Counter {
value: i32,
}
#[derive(Debug, Clone, Copy)]
enum Message {
Increment,
Decrement,
}
impl Sandbox for Counter {
type Message = Message;
fn new() -> Self {
Self::default()
}
fn title(&self) -> String {
String::from("A cool counter")
}
fn update(&mut self, message: Message) {
match message {
Message::Increment => {
self.value += 1;
}
Message::Decrement => {
self.value -= 1;
}
}
}
fn view(&self) -> Element<Message> {
column![
button("+").on_press(Message::Increment),
text(self.value).size(50),
button("-").on_press(Message::Decrement),
]
.into()
}
}
fn main() -> iced::Result {
Counter::run(Settings::default())
}
Complex State Management and Layout
use iced::widget::{button, column, container, row, text, text_input};
use iced::{Alignment, Element, Length, Sandbox, Settings};
#[derive(Default)]
struct TodoApp {
todos: Vec<Todo>,
input: String,
}
#[derive(Clone, Debug)]
struct Todo {
id: usize,
text: String,
completed: bool,
}
#[derive(Debug, Clone)]
enum Message {
AddTodo,
InputChanged(String),
ToggleTodo(usize),
DeleteTodo(usize),
ClearCompleted,
}
impl Sandbox for TodoApp {
type Message = Message;
fn new() -> Self {
Self::default()
}
fn title(&self) -> String {
String::from("Todo App - Iced")
}
fn update(&mut self, message: Message) {
match message {
Message::AddTodo => {
if !self.input.trim().is_empty() {
let id = self.todos.len();
self.todos.push(Todo {
id,
text: self.input.clone(),
completed: false,
});
self.input.clear();
}
}
Message::InputChanged(value) => {
self.input = value;
}
Message::ToggleTodo(id) => {
if let Some(todo) = self.todos.iter_mut().find(|t| t.id == id) {
todo.completed = !todo.completed;
}
}
Message::DeleteTodo(id) => {
self.todos.retain(|todo| todo.id != id);
}
Message::ClearCompleted => {
self.todos.retain(|todo| !todo.completed);
}
}
}
fn view(&self) -> Element<Message> {
let input_section = row![
text_input("What needs to be done?", &self.input)
.on_input(Message::InputChanged)
.on_submit(Message::AddTodo)
.padding(10),
button("Add")
.on_press(Message::AddTodo)
.padding(10)
]
.spacing(10)
.align_items(Alignment::Center);
let todos_section = column![{
let mut col = column![];
for todo in &self.todos {
let todo_row = row![
button(if todo.completed { "✓" } else { "○" })
.on_press(Message::ToggleTodo(todo.id)),
text(&todo.text).width(Length::Fill),
button("×")
.on_press(Message::DeleteTodo(todo.id))
]
.spacing(10)
.align_items(Alignment::Center);
col = col.push(todo_row);
}
col
}]
.spacing(5);
let controls = row![
text(format!("Total: {}", self.todos.len())),
button("Clear Completed")
.on_press(Message::ClearCompleted)
]
.spacing(20)
.align_items(Alignment::Center);
container(
column![
text("Todo List")
.size(24)
.width(Length::Fill)
.horizontal_alignment(iced::alignment::Horizontal::Center),
input_section,
todos_section,
controls
]
.spacing(20)
.padding(20)
.max_width(600)
)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
}
}
fn main() -> iced::Result {
TodoApp::run(Settings::default())
}
Asynchronous Processing and Network Communication
use iced::widget::{button, column, container, text};
use iced::{executor, Application, Command, Element, Settings, Theme};
use std::time::Duration;
#[derive(Default)]
struct AsyncApp {
status: String,
is_loading: bool,
}
#[derive(Debug, Clone)]
enum Message {
FetchData,
DataFetched(Result<String, String>),
StartTimer,
TimerFinished,
}
impl Application for AsyncApp {
type Executor = executor::Default;
type Message = Message;
type Theme = Theme;
type Flags = ();
fn new(_flags: ()) -> (Self, Command<Message>) {
(Self::default(), Command::none())
}
fn title(&self) -> String {
String::from("Async Example")
}
fn update(&mut self, message: Message) -> Command<Message> {
match message {
Message::FetchData => {
self.is_loading = true;
self.status = "Fetching data...".to_string();
Command::perform(fetch_data(), Message::DataFetched)
}
Message::DataFetched(result) => {
self.is_loading = false;
match result {
Ok(data) => self.status = format!("Success: {}", data),
Err(error) => self.status = format!("Error: {}", error),
}
Command::none()
}
Message::StartTimer => {
self.status = "Timer started...".to_string();
Command::perform(
async {
tokio::time::sleep(Duration::from_secs(3)).await;
},
|_| Message::TimerFinished,
)
}
Message::TimerFinished => {
self.status = "Timer finished!".to_string();
Command::none()
}
}
}
fn view(&self) -> Element<Message> {
container(
column![
text("Async Operations Example").size(24),
text(&self.status).size(16),
button("Fetch Data")
.on_press_maybe(if self.is_loading { None } else { Some(Message::FetchData) }),
button("Start Timer")
.on_press(Message::StartTimer),
]
.spacing(20)
.padding(20)
)
.width(iced::Length::Fill)
.height(iced::Length::Fill)
.center_x()
.center_y()
.into()
}
}
async fn fetch_data() -> Result<String, String> {
// Simulate HTTP request
tokio::time::sleep(Duration::from_secs(2)).await;
Ok("Data from server".to_string())
}
fn main() -> iced::Result {
AsyncApp::run(Settings::default())
}
Creating Custom Widgets
use iced::widget::canvas::{self, Canvas, Cursor, Geometry, Path, Stroke};
use iced::widget::{column, container, slider};
use iced::{
Color, Element, Length, Point, Rectangle, Renderer, Sandbox, Settings, Size, Theme, Vector,
};
#[derive(Default)]
struct CircleApp {
radius: f32,
color_hue: f32,
}
#[derive(Debug, Clone)]
enum Message {
RadiusChanged(f32),
ColorChanged(f32),
}
impl Sandbox for CircleApp {
type Message = Message;
fn new() -> Self {
Self {
radius: 50.0,
color_hue: 0.0,
}
}
fn title(&self) -> String {
String::from("Custom Canvas Widget")
}
fn update(&mut self, message: Message) {
match message {
Message::RadiusChanged(radius) => {
self.radius = radius;
}
Message::ColorChanged(hue) => {
self.color_hue = hue;
}
}
}
fn view(&self) -> Element<Message> {
container(
column![
Canvas::new(CircleCanvas {
radius: self.radius,
color_hue: self.color_hue,
})
.width(Length::Fill)
.height(Length::Fixed(300.0)),
slider(10.0..=100.0, self.radius, Message::RadiusChanged)
.step(1.0),
slider(0.0..=360.0, self.color_hue, Message::ColorChanged)
.step(1.0),
]
.spacing(20)
.padding(20)
)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
}
}
struct CircleCanvas {
radius: f32,
color_hue: f32,
}
impl canvas::Program<Message> for CircleCanvas {
type State = ();
fn draw(
&self,
_state: &Self::State,
renderer: &Renderer,
_theme: &Theme,
bounds: Rectangle,
_cursor: Cursor,
) -> Vec<Geometry> {
let circle = Path::circle(bounds.center(), self.radius);
// HSV to RGB conversion (simplified)
let hue = self.color_hue / 60.0;
let c = 1.0;
let x = 1.0 - (hue % 2.0 - 1.0).abs();
let (r, g, b) = match hue as i32 {
0 => (c, x, 0.0),
1 => (x, c, 0.0),
2 => (0.0, c, x),
3 => (0.0, x, c),
4 => (x, 0.0, c),
_ => (c, 0.0, x),
};
let color = Color::from_rgb(r, g, b);
vec![canvas::Geometry::from_path(
circle,
canvas::Fill::Solid(color),
)]
}
}
fn main() -> iced::Result {
CircleApp::run(Settings::default())
}
Web Application Support
// Cargo.toml configuration
// [dependencies]
// iced = { version = "0.10", features = ["canvas", "tokio"] }
//
// [target.'cfg(target_arch = "wasm32")'.dependencies]
// iced = { version = "0.10", features = ["canvas"] }
use iced::widget::{button, column, container, text};
use iced::{Element, Length, Sandbox, Settings};
#[derive(Default)]
struct WebApp {
counter: i32,
message: String,
}
#[derive(Debug, Clone)]
enum Message {
Increment,
Decrement,
Reset,
}
impl Sandbox for WebApp {
type Message = Message;
fn new() -> Self {
Self {
counter: 0,
message: "Iced app running on the web".to_string(),
}
}
fn title(&self) -> String {
String::from("Iced Web App")
}
fn update(&mut self, message: Message) {
match message {
Message::Increment => {
self.counter += 1;
self.message = format!("Counter: {}", self.counter);
}
Message::Decrement => {
self.counter -= 1;
self.message = format!("Counter: {}", self.counter);
}
Message::Reset => {
self.counter = 0;
self.message = "Reset completed".to_string();
}
}
}
fn view(&self) -> Element<Message> {
container(
column![
text("Iced Web Application").size(24),
text(&self.message).size(16),
text(self.counter).size(48),
column![
button("+ Increment").on_press(Message::Increment),
button("- Decrement").on_press(Message::Decrement),
button("Reset").on_press(Message::Reset),
]
.spacing(10)
]
.spacing(20)
.padding(40)
)
.width(Length::Fill)
.height(Length::Fill)
.center_x()
.center_y()
.into()
}
}
fn main() -> iced::Result {
WebApp::run(Settings::default())
}
Project Setup and Build
# Create new Rust project
cargo new iced-app
cd iced-app
# Cargo.toml dependencies
# [dependencies]
# iced = "0.10"
# tokio = { version = "1.0", features = ["full"] }
# Run native app
cargo run
# Release build
cargo build --release
# Web application setup
rustup target add wasm32-unknown-unknown
cargo install trunk
# Create index.html file
cat > index.html << 'EOF'
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Iced Web App</title>
<style>
body {
margin: 0;
padding: 0;
width: 100vw;
height: 100vh;
overflow: hidden;
}
canvas {
outline: none;
}
</style>
</head>
<body></body>
</html>
EOF
# Build and run web app
trunk serve --release
# Build with different rendering backends
# WebGPU (recommended)
cargo run --features wgpu
# OpenGL
cargo run --features glow
# Software rendering
cargo run --features tiny-skia
# Cross-platform builds
# For Windows
cargo build --target x86_64-pc-windows-gnu --release
# For macOS
cargo build --target x86_64-apple-darwin --release
# For Linux
cargo build --target x86_64-unknown-linux-gnu --release