Library / SDK
iced-rs/iced avatar
iced-rs/iced

iced: an Elm-shaped GUI library for Rust, and what its modular split costs you

A cross-platform GUI library for Rust, inspired by Elm

31,496 stars1,660 forksRustMIT

At a glance

What is it?
Iced puts state, messages, view and update at the centre of a Rust desktop app and ships two renderers behind a renderer-agnostic runtime. The design is coherent; the crate boundaries and the 0.x versioning are the parts to weigh before adopting.
Who is it for?
Adopt iced if you are building a Rust desktop or Web app and you want the Elm architecture enforced by the type system rather than bolted on. Do not adopt it if you need a stable API surface today: the README states the project is experimental software, the current line is 0.14.0, and there was a gap of roughly fifteen months between 0.13.1 and 0.14.0.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly Rust, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on September 15, 2026, and from our analysis. They are not legal advice.

DEEP OPEN-SOURCE ANALYSIS

The problem iced solves: UI state that Rust's type system can hold onto

Most GUI toolkits let you mutate widget trees in place. Rust makes that awkward: shared mutable UI state fights the borrow checker, and the usual workarounds (interior mutability, reference-counted handles) push errors from compile time to runtime. Iced sidesteps this by borrowing a pattern from Elm. The README says the library expects you to split a user interface into four concepts: state, messages, view logic, and update logic. Your application state is an ordinary struct. Interactions are an enum. The view function takes &self and returns widgets. The update function takes &mut self and a message. Nothing in that loop requires a widget to outlive a frame or to be mutated from two places.

That is the audience. If you are writing a desktop tool in Rust and you have already accepted that your UI needs a message loop, iced gives you one with a shape the compiler can check. It is less useful if you are embedding a few controls into an existing C++ or Python application, or if your interface is a canvas you draw into yourself; the README's framing is batteries-included and end-user-oriented, which implies iced wants to own the window.

State, messages, view, update: the actual loop

The README's counter example is the whole mechanism in miniature, and it is worth reading literally. State is a struct with a single i32. Messages are an enum with Increment and Decrement. The view returns a Column built with the column! macro, containing a button("+") with .on_press(Message::Increment), a text(self.value).size(50), and a button("-") with .on_press(Message::Decrement). The update method matches on the message and adjusts the value. main returns iced::Result and calls iced::run(Counter::update, Counter::view).

What iced does with those two function pointers is stated in the README as three automatic steps: take the result of the view logic and lay out its widgets; process events from the system and produce messages for the update logic; draw the resulting user interface. So the data flow is one-directional. Events become messages, messages mutate state, state produces a new view, the view is laid out and drawn. There is no path by which a widget writes back into your struct.

The type signature of view is the interesting part: Column<'_, Message>. The lifetime is borrowed from self, which means the widget tree cannot outlive the state that produced it. That is the constraint that makes the whole thing work in safe Rust, and it is also why you cannot easily stash a widget somewhere and reuse it across frames.

Two renderers behind one runtime, and why the crate split matters

The README describes a modular ecosystem split into reusable parts: a renderer-agnostic native runtime, two built-in renderers, and a windowing shell. The renderers are iced_wgpu, which the README says supports Vulkan, Metal and DX12, and iced_tiny_skia, a software alternative positioned as a fallback. The windowing shell is built on winit.

This split is the most consequential architectural decision in the project, more than the Elm pattern itself. It means the same application code can be drawn by a GPU backend or by a CPU rasteriser, and the runtime does not care which. For deployment that matters: a machine with no usable Vulkan or Metal driver is not necessarily a dead end, because tiny-skia exists as a fallback path. The README does not state how the two are selected at runtime, so if that choice is important to you, read the runtime and renderer crates rather than the README.

The cost of the split is that iced is not one dependency. The README lists the parts as separate directories in the repository (runtime/, wgpu/, tiny_skia/, winit/), each presumably its own crate. Depending on iced directly gets you the batteries-included facade; depending on the pieces gets you control and a wider surface to track across releases. For most applications the facade is the right call, and the modular layout is only worth reaching into if you are integrating with an existing system, which is exactly the use case the README names for the renderer-agnostic runtime.

Getting a counter on screen: the commands the README gives

The README provides one runnable path. You define the struct, the message enum, the view method and the update method as shown, then write a main that returns iced::Result and calls iced::run(Counter::update, Counter::view). That is the entire bootstrap: no Application trait implementation is shown, no builder, no explicit window configuration. The README's phrasing is that iced will automatically handle layout, event processing and drawing.

Beyond that, the README does not give a Cargo.toml snippet, does not name feature flags, and does not show how to select a renderer or configure a window title or size. Those details live in the linked book at book.iced.rs and the documentation at docs.rs/iced, both of which the README points to, and in the examples directory. If you need a concrete starting point rather than a from-scratch counter, the examples directory is the place the README sends you.

One thing the README does establish about the API surface: widgets are configured by chaining methods, as in button("+").on_press(Message::Increment) and text(self.value).size(50). The column! macro builds layout. That builder style is consistent across the built-in widgets the README names: text inputs, scrollables, and others it does not enumerate.

The 0.x problem, and the gap between releases

The README says plainly: Iced is currently experimental software. That sentence is doing real work, and it sits directly above a link to the roadmap and a link to the issue tracker. Anyone evaluating iced for production should treat that as the headline rather than the feature list.

The release history supports the caution. The current line is 0.14.0, published in December 2025. Before it, 0.13.0 and 0.13.1 landed within a day of each other in September 2024, and then there is a gap of roughly fifteen months to 0.14.0. A long gap followed by a minor-version bump is a normal shape for a volunteer-maintained GUI library, but it has a practical consequence: if you pin 0.13.x today, you may be sitting on it for a while, and when 0.14.0's successors arrive you should expect to read migration notes rather than assume a drop-in upgrade. The README does not include changelogs, so I cannot tell you what changed between 0.13.1 and 0.14.0. Check the release notes before upgrading.

There is a second, subtler cost. Because iced is split into runtime, renderers and shell, a breaking change in the runtime can ripple outward even if the facade crate's own API looks stable. If you depend only on iced, you absorb that once. If you depend on iced_wgpu or the windowing shell directly, you absorb it per crate.

Where iced is the wrong choice

The Elm architecture is a constraint, not a convenience. If your interface does not decompose into state plus discrete messages, you will spend your time fighting it. A drawing canvas, a code editor with its own text buffer and selection model, or anything with high-frequency per-frame input is a poor fit for a model where every interaction is supposed to become a message. The README's own examples are telling: a counter, a tour, a todo list, a text input, a scrollable. Those are the shapes the library is designed around.

Accessibility is not mentioned anywhere in the README. Iced is cross-platform across Windows, macOS, Linux and the Web, but the README does not describe a screen-reader bridge or an accessibility tree. If your application has an accessibility requirement, that silence is a reason to investigate before you commit, not after.

The experimental status cuts the other way too. If your project needs a frozen API you can build on for years without touching it, iced's 0.x line and the fifteen-month gap between 0.13.1 and 0.14.0 are a poor match. A team that cannot absorb a migration should look elsewhere or budget for the work explicitly.

The alternative: immediate-mode GUI, and the real difference

The natural comparison is with an immediate-mode Rust GUI library in the egui family. The difference is not cosmetic; it is a different answer to the same borrow-checker problem. In an immediate-mode library, you re-declare the entire interface every frame inside a closure that has access to your state, and the library diffs or redraws from that. There is no message enum, no separate update function, and no view function returning widgets that borrow state.

That changes what you write. Immediate mode tends to be faster to prototype for tool-like interfaces, because there is no ceremony around defining messages for every interaction. Iced's model tends to be easier to reason about as an application grows, because every state transition is a named variant handled in one match statement, and the compiler will tell you when you have missed one. Iced also gets you a retained widget tree with responsive layout, which the README lists as a feature, and a renderer-agnostic runtime that an immediate-mode library of egui's shape does not offer in the same form.

The trade is real in both directions. If you want to add a control to an existing window and move on, the immediate-mode approach is less code. If you want to know, at compile time, every way your application's state can change, iced's message enum is the stronger tool. Choose based on which of those two you actually need, not on which one has a nicer counter example.

Maintenance, licence and what to verify before you pin

Iced is MIT licensed, and the README carries a licence badge pointing at the LICENSE file on master. MIT is permissive: it lets you use, modify and redistribute the code, including in closed-source products, provided the copyright notice and permission notice are preserved. That is the general shape of the licence, not legal advice; read the LICENSE text at the tag you pin, because the badge in a README is not the licence.

The maintenance picture is a sponsored open-source project. The README states that development is sponsored by the Cryptowatch team at Kraken.com, and it links a contributing guide, a Zulip forum and a Discord server. A corporate sponsor is a meaningful signal about continuity, but it is not a support contract, and the README does not describe any commercial support offering.

Upgrade cost is where the crate split bites. If you depend on iced alone, an upgrade is one version bump plus whatever API changes the release notes describe. If you depend on the runtime, a renderer, or the windowing shell directly, you are tracking several version numbers that may not move in lockstep. Before adopting, the concrete things to check are: the ROADMAP.md entries for the crates you intend to name in Cargo.toml, the release notes for 0.14.0 to see what changed since 0.13.1, and the examples directory for whichever widget is closest to your hardest screen. That last one is the cheapest way to find out whether your interface fits the four-concept model at all.

Editorial conclusion

Adopt iced if you are building a Rust desktop or Web app and you want the Elm architecture enforced by the type system rather than bolted on. Do not adopt it if you need a stable API surface today: the README states the project is experimental software, the current line is 0.14.0, and there was a gap of roughly fifteen months between 0.13.1 and 0.14.0. Before committing, check the ROADMAP.md entries for the crates you intend to depend on directly (iced_wgpu, iced_tiny_skia, the runtime, the winit shell), confirm which of them you actually need to name in Cargo.toml, and read the examples directory for the widget closest to your hardest screen. Verify the licence file at the tag you pin, since the MIT badge in the README is not a substitute for the LICENSE text.

Official sources

  1. iced-rs/iced on GitHub
  2. License: MIT
  3. Project website
  4. README
  5. Releases
Community notes

Community notes