Rust GUI frameworks
2022 Framework 2026: Niche, active, pre-stable
22

Makepad and Freya

Two more GPU-first takes on the same problem.

Makepad and Freya are both GPU-era toolkits that refuse to share much with the rest of the stack, in very different ways. Makepad builds everything itself: its own windowing, its own rendering, its own layout and its own shader language. Its lockfile contains no winit, no wgpu, no Taffy and no AccessKit. You write the UI in a live-reloading DSL, live_design!, and handle events in Rust.

makepad-app/src/bin/one.rs ✔ compiled · makepad-widgets 1.0 · rustc 1.95.0
use makepad_widgets::*;

live_design! {
    use link::widgets::*;

    App = {{App}} {
        ui: <Root> {
            <Window> {
                body = <View> {
                    flow: Down, spacing: 20, align: { x: 0.5, y: 0.5 }
                    button1 = <Button> { text: "Click me" }
                    label1 = <Label> { text: "Clicked 0 times" }
                }
            }
        }
    }
}

app_main!(App);

fn main() {
    app_main()
}

#[derive(Live, LiveHook)]
pub struct App {
    #[live]
    ui: WidgetRef,
    #[rust]
    counter: usize,
}

impl LiveRegister for App {
    fn live_register(cx: &mut Cx) {
        makepad_widgets::live_design(cx);
    }
}

impl MatchEvent for App {
    fn handle_actions(&mut self, cx: &mut Cx, actions: &Actions) {
        if self.ui.button(id!(button1)).clicked(actions) {
            self.counter += 1;
            self.ui.label(id!(label1)).set_text(cx, &format!("Clicked {} times", self.counter));
        }
    }
}

impl AppMain for App {
    fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
        self.match_event(cx, event);
        self.ui.handle_event(cx, event, &mut Scope::empty());
    }
}

The DSL is also where Makepad's distinctive feature lives: shaders are written inline. The pixel function below is a GPU fragment shader, in Makepad's own shading language, that styles a plain View with a gradient.

makepad-app/src/bin/two.rs ✔ compiled · makepad-widgets 1.0 · rustc 1.95.0
use makepad_widgets::*;

// Makepad widgets are drawn by shaders you can write inline. This is a GPU
// fragment shader, in Makepad's own shading language, styling a plain View.
live_design! {
    use link::widgets::*;

    App = {{App}} {
        ui: <Root> {
            <Window> {
                body = <View> {
                    show_bg: true
                    draw_bg: {
                        fn pixel(self) -> vec4 {
                            return mix(#e5484d, #30a46c, self.pos.x);
                        }
                    }
                }
            }
        }
    }
}

app_main!(App);

fn main() {
    app_main()
}

#[derive(Live, LiveHook)]
pub struct App {
    #[live]
    ui: WidgetRef,
}

impl LiveRegister for App {
    fn live_register(cx: &mut Cx) {
        makepad_widgets::live_design(cx);
    }
}

impl AppMain for App {
    fn handle_event(&mut self, cx: &mut Cx, event: &Event) {
        self.ui.handle_event(cx, event, &mut Scope::empty());
    }
}

Freya went the other way and is easy to misdescribe. Earlier versions were "Dioxus plus Skia". Since 0.4 (16 July 2026) it has its own reactive and component model, no dioxus-core in its lockfile, and its own layout engine (torin). It does still render with Skia, through a fork called freya-skia-safe, with winit for windows and AccessKit for screen readers. UI is built by chaining plain Rust methods, with no macro.

freya-app/src/bin/one.rs ✔ compiled · freya 0.4 · rustc 1.95.0
use freya::prelude::*;

fn main() {
    launch(LaunchConfig::new().with_window(WindowConfig::new(app)))
}

fn app() -> impl IntoElement {
    let mut count = use_state(|| 0);

    rect()
        .expanded()
        .center()
        .spacing(8.0)
        .child(format!("clicked {} times", count.read()))
        .child(
            Button::new()
                .on_press(move |_| *count.write() += 1)
                .child("Increase"),
        )
}
crates.io first publish.