WaterUI writes one set of views and lets the backend pick the toolkit
Bring your app to all platform, learning once and applying anywhere
At a glance
- What is it?
- WaterUI is a Rust UI framework where views are written once and each backend maps them onto UIKit and AppKit on Apple, GTK4 on Linux, or the self-drawn Hydrolysis renderer on Android and the GPU. State is plain values in a Binding rather than a virtual tree, and the project is pre-1.0 and says it breaks its API on purpose.
- Who is it for?
- Adopt WaterUI if you are writing a Rust application and want to ship the same views on iOS, Android, Linux, macOS and Windows, and you can accept a pre-1.0 API. Do not adopt it if you need a stable interface today, because the project states the API still moves and that it breaks it on purpose, and the Linux backend is the youngest of the three.
- Can I use it commercially?
- Yes. Apache-2.0 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 3 days 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 October 20, 2026, and from our analysis. They are not legal advice.
Editorial analysis
One view description, several toolkits, and one self-drawn renderer
WaterUI is a cross-platform UI framework for Rust. The pitch in the header is short: bring your app to all platforms, learning once and applying anywhere.
The mechanism is a set of backends that map the same view descriptions onto whatever the platform actually uses. UIKit and AppKit on Apple, GTK4 on Linux, and Hydrolysis as the self-drawn renderer where no native toolkit fits. Hydrolysis draws through Android's hardware-accelerated renderer, HWUI, on Android, and on the GPU through Cherenkov elsewhere.
The backend table in the README is the clearest summary of the whole design.
| Target | Backend | Renders through | | --- | --- | --- | | iOS and macOS | Apple | `UIKit` / `AppKit` | | Android | Hydrolysis | Self-drawn, HWUI `RenderNode` display lists | | Linux | GTK4 | GTK4 widgets | | macOS, Linux, Windows, web | Hydrolysis | Self-drawn, GPU (Cherenkov) |
Two things to notice in that table. First, the target and the backend are independent: macOS can go through the Apple backend or through Hydrolysis, and the backend is chosen per command. Second, the last row shows the same self-drawn renderer serving Windows and the web, which is what makes the cross-platform claim plausible at all rather than aspirational.
So the framework gives you a description of an interface, and a backend decides whether that becomes native widgets on a platform that has them or drawn pixels on one that does not.
State is plain values, and there is no virtual tree to diff
The state model is where a Rust UI framework has to make a real choice, and WaterUI's is unusual enough to be worth understanding before you write anything.
State is plain values. You put mutable state in a Binding, derive from it with a Computed, and hand those to views. When a value changes, the views that read it update.
The claim that follows is that there is no virtual tree to diff, and that changing one string never rebuilds the subtree around it.
That is the whole reactive argument, and it is a different shape from the one most UI toolkits use. A virtual DOM diffs a tree against the previous tree to work out what changed. A Binding-based model tracks the dependency graph directly, so the update scope is the set of views that actually read the changed value, not a diff of the surrounding structure.
The API shows how that works in practice.
fn progress_editor() -> impl View {
let value = Binding::f64(0.25);
let percent = value.map(|value| value * 100.0);
vstack((
slider("Progress", &value).range(0.0..=1.0),
text!("Progress: {percent:.0}%"),
))
}The percent value is derived from the slider's value rather than stored, so it cannot get out of sync. And the text view reads it, so it updates when the slider moves, without the slider and the label being aware of each other.
The brief also says signal-aware APIs take either a Binding or a Computed, and only their readers update. That is the granular part: the reader is what subscribes.
watch replaces a subtree, and that is the one sharp edge
One thing to know early, as the documentation puts it: watch replaces the subtree it wraps, losing any state inside it. It exists for intentional structural swaps.
For routine updates, pass signals into components and let the framework do the precise thing.
That is a genuine footgun and the documentation is upfront about it. In a framework where the normal update path is precise and incremental, an API that silently discards the state of everything beneath it is the sort of thing you use once and regret. The README frames it as being for intentional structural swaps, which is a reasonable framing, but it means watch is not the general replacement for a conditional render and you need to know which one you are reaching for.
Collections work the same reactive way. Give List or ForEach a reactive collection of Identifiable items and membership changes are diffed by id.
#[derive(Clone, Identifiable)]
struct Contact {
#[id]
id: u64,
name: &'static str,
}The id attribute is what makes the diff meaningful. Diffing by id rather than by position means inserting an item at the top of a list does not invalidate the state of everything below it, which is the same principle the Binding model applies elsewhere: identity is explicit, and the framework uses it to scope the update.
It is a small design decision that a lot of the ergonomics rest on, and it is the kind of thing that is cheap to get right at the start and expensive to retrofit.
The CLI owns the native projects, your crate stays small
The install path is a CLI that downloads a prebuilt binary, so nothing is compiled to get started.
curl --proto '=https' --tlsv1.2 -LsSf https://github.com/water-rs/waterui/releases/download/cli-latest/waterui-cli-installer.sh | shOn Windows the installer is a PowerShell one-liner.
powershell -ExecutionPolicy Bypass -c "irm https://github.com/water-rs/waterui/releases/download/cli-latest/waterui-cli-installer.ps1 | iex"Then creating and running a project is three commands.
water create counter
cd counter
water runThe interesting architectural decision is what happens next. A WaterUI project is a plain Rust crate, and the CLI keeps native projects out of your source tree and manages them on demand. The app crate depends on waterui and nothing else, with FFI glue and backend projects generated and owned by the CLI.
That is a serious simplification of a problem that normally goes wrong. Cross-platform Rust UI tools often require you to keep generated Xcode projects, Gradle projects and CMake directories in your repository, and those files rot. Here they are generated on demand, so the repository contains your code and a manifest.
The manifest is Water.toml, which holds package metadata, permissions and theming, and the CLI builds, signs and packages the app from the declarations in it. The same project you iterate on is the one that ships.
There is also a doctor and a devices command. water doctor tells you what is missing from a toolchain, and water devices lists simulators and connected devices, which are the two things that actually block a first run on a new platform.
Previews, tests and measurements from the same surface
The preview mechanism is the part of the framework that most changes how you work, and it is not a screenshot feature.
Any function marked with the preview attribute can be rendered to an image without launching the app.
water preview main --output preview.png
water preview main --frame 800x600 --output preview.pngThe frame flag fixes the output size, which is what you need for anything that compares images across runs.
Then the same surface drives two other things. There is a preview test mode for semantic interaction tests, and a preview perf mode for GPU measurements.
The distinction between semantic interaction tests and a screenshot comparison is worth spelling out. A screenshot test asserts that pixels match, which breaks whenever a font, a driver or a platform version changes underneath you. A semantic interaction test drives the view and asserts on behaviour, so it survives a rendering change but tells you nothing about whether the result looks right. Having both available from the same rendering surface means you can choose per test, and having the surface at all means you can produce a view for a test without a running app, a simulator or a device.
For a framework that has to look the same on four toolkits, that is a substantial investment, and it is also what makes the per-platform backend differences something you can see rather than discover in production.
Pre-1.0, deliberately unstable, with an uneven backend maturity
The Status section is short and it is the most important thing in the documentation for anyone planning to adopt this.
Pre-1.0. The API still moves, and we break it on purpose when a better shape is found.
That is an unusual thing for a project to state plainly, and it means your upgrade cost is not zero. A framework that breaks its API when it finds a better shape will save you from a bad design later, and it will cost you a refactor now.
Then the maturity ranking of the backends, which is the fact an adopter needs most: the Apple backend is the most complete, Hydrolysis, which also serves Android, is close behind, and GTK4 is younger.
So on Linux you are on the youngest backend, and on Linux you also have the choice of skipping GTK4 altogether and using Hydrolysis on the GPU instead. That choice is per command, so a project can use GTK4 for native widget behaviour on some targets and Hydrolysis for a consistent look elsewhere, at the cost of the two looking different.
The docs also ask for issues: if you hit a wall, an issue with a small reproduction is genuinely useful. For a pre-1.0 project with an incomplete backend, that is the whole feedback loop.
The repository layout backs the scale of the effort. The workspace members list runs to dozens of crates across core, components, backends, graphics, cli, testing and macros, with separate families under graphics for the Cherenkov renderer and the filtrate image processing, and the Apple backend has its own Cocoa UI subproject and an iOS test host.
Editorial conclusion
Adopt WaterUI if you are writing a Rust application and want to ship the same views on iOS, Android, Linux, macOS and Windows, and you can accept a pre-1.0 API. Do not adopt it if you need a stable interface today, because the project states the API still moves and that it breaks it on purpose, and the Linux backend is the youngest of the three. Verify first which backend you will actually ship on, because the Apple backend is the most complete, Hydrolysis is close behind, and GTK4 is younger, and the differences show up in what you can build.
Frequently asked questions
What is WaterUI?
It is a cross-platform UI framework for Rust where you write views once and each backend maps them onto the platform's toolkit, UIKit and AppKit on Apple, GTK4 on Linux, or the self-drawn Hydrolysis renderer on Android and the GPU. The homepage is waterui.dev and the licence is MIT or Apache-2.0.
How do I install the WaterUI CLI?
Use the installer script, which downloads a prebuilt binary so nothing is compiled. On macOS and Linux it is the curl command piped to sh, and on Windows it is the PowerShell irm command piped to iex.
How does state work in WaterUI?
State is plain values. Mutable state goes in a Binding, derived state comes from Computed via map, and views that read a value update when it changes. There is no virtual tree to diff, and signal-aware APIs accept either kind, with only their readers updating.
Is WaterUI stable enough to depend on?
Not yet. The project states it is pre-1.0, that the API still moves and that it breaks the API on purpose when a better shape is found. The Apple backend is the most complete, Hydrolysis is close behind, and GTK4 is younger.
What can the water preview command do?
Any function marked with the preview attribute can be rendered to an image without launching the app, with an optional frame size. The same surface also drives preview test for semantic interaction tests and preview perf for GPU measurements.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/water-rs-waterui)Was this useful?