Why Rust
Why RustThe standard answer is memory safety, no garbage collector, and native compilation. That's true, and reason enough. But the deeper reason idealyst is written in Rust is that the language's shape fits UI authoring in ways most languages don't.
The boilerplate, brieflyRust gives you memory safety without a garbage collector, ahead-of-time compilation to native code on every target, and zero-cost abstractions. UI frameworks are unusually sensitive to all three: GC pauses show up as dropped frames, runtime overhead competes with the rendering hot path, and bundle size matters on web. Picking Rust dodges all three at once.That's the elevator pitch. Most other Rust pitches stop here. The rest of this page is the part that doesn't usually get said.
The shape of the language fits UIBeyond the runtime properties, Rust has a set of language-design choices that line up unusually well with how UI authoring actually works. Most of these aren't unique to Rust on their own, but the combination — expressions, pattern matching, enums, ownership, traits, and a real macro system — compose into a language that doesn't fight you when you're writing components.
Expressions, not statementsUI is a function of state. State branches; the view branches with it. In a language where `if` is a statement, every branch forces you to introduce a mutable variable, conditionally assign it, then reference it downstream — three steps for what's fundamentally one transformation.In Rust, every block is an expression. `if`, `match`, `loop`, and braced blocks all evaluate to a value, and that value can flow directly into a component prop, a `let` binding, or a child slot.
// Every block in Rust evaluates to a value.
let label = if count > 9 { "9+".to_string() } else { count.to_string() };
ui! { text { label } }

// Compare to a language where `if` is a statement:
let label;
if (count > 9) {
    label = "9+";
} else {
    label = String(count);
}
return <span>{label}</span>;
Pattern matching as a render switchThe same expression-orientation extends to enumerated states. Loading? Loaded? Error? A `match` over your state enum is the natural shape for picking what to render, and the matched data is destructured inline so the render branch sees the typed payload immediately.The compiler enforces exhaustiveness. Add a new variant later — `FetchState::Cached(stale_data)` — and every `match` site in the codebase is a compile error until you handle it. The class of bugs where you add a new state and forget to render it doesn't survive `cargo build`.
let view = match fetch_state.get() {
    FetchState::Idle => idle_view(),
    FetchState::Loading => spinner(),
    FetchState::Loaded(data) => results_view(data),
    FetchState::Error(msg) => error_view(msg),
};
Enums make invalid states unrepresentablePair `match` with Rust's sum types and a whole class of state bugs disappears. The typical JavaScript loading state is a bag of optional flags, and every combination of `loading`, `data`, and `error` is a constructible value — including the nonsensical ones.Rust's enum says the state is exactly one of four shapes. You cannot construct "loading and loaded simultaneously" because the type doesn't admit it. UI state stops drifting into impossible territory.
// In JavaScript, every combination is reachable:
{ loading: true,  data: null,    error: null    }  // valid
{ loading: false, data: result,  error: null    }  // valid
{ loading: false, data: null,    error: "..."   }  // valid
{ loading: true,  data: result,  error: "..."   }  // also valid?!

// In Rust, the type system forbids the nonsense:
enum FetchState<T> {
    Idle,
    Loading,
    Loaded(T),
    Error(String),
}
Macros over explicit code`ui!` is a macro. So are `stylesheet!`, `#[component]`, `signal!`, and the rest of idealyst's authoring surface. Each is syntactic sugar that desugars to plain function calls against props structs.The macro is opt-in. You can drop down to the explicit form any time and the framework is identical — same primitives, same signals, same output. JSX in React started as the obvious way and quietly became the only way. Rust's macros are sugar by construction; the explicit form is always legible and writable.This matters for tooling and inspection. When a build error mentions a type mismatch, you can read the desugared call directly without learning a second mental model.
// What you write:
ui! {
    button(label = "Hi".to_string(), on_click = on_press)
}

// What the macro expands to:
button(&ButtonProps {
    label: "Hi".to_string(),
    on_click: on_press,
    ..Default::default()
})
Closures with explicit captureEvent handlers are closures. `move ||` captures by ownership; without `move`, the closure borrows. The keyword is surface-visible — you know exactly which signals the handler holds onto, and the borrow checker enforces it.React's stale-closure bug — an effect or callback capturing an old render's variables — is a direct consequence of implicit capture. In Rust, the moment you write `move`, you've declared the boundary, and the framework can store the closure and call it later with no risk of pointing at a stale binding.
// `move` is explicit — you see what the closure captures.
let on_click = move || count.update(|n| *n += 1);

// Compare JavaScript, where capture is implicit:
const onClick = () => setCount(c => c + 1);
// — which render's `setCount` does this point to?
// — if the parent re-renders, does this closure still work?
Ownership for refs and handlesA `Ref<ButtonHandle>` in idealyst doesn't expose the handle directly. The read API is a closure: you hand the ref a function that takes the handle, and the framework runs it only if the button is mounted right now.The shape is enforced at the type level. There's no way to extract a raw handle and call a method on it later — the borrow can't outlive the closure. A whole category of "used a ref after the component unmounted" crashes is gone.
// You don't get the handle directly. You read it through a closure:
btn_ref.with(|handle| handle.focus());

// Returns Option<R> — `None` when the button isn't mounted.
// There's no way to get a raw handle out and call .focus() on it
// later, after the button might have been torn down.
Traits, not inheritance`Backend` is a trait. So is `IdeaTheme`, `IntoStyleSource`, `RouteParams`, `IntoElement`. Adding a new platform, a new theme implementation, or a new style source means implementing a trait — no class extension, no method-override resolution, no diamond inheritance, no `super` calls.Composition over inheritance isn't a moral guideline in Rust; it's the only option the language gives you. The framework's contracts are surface- visible because every contract is a trait you can read top to bottom in one file.
The macro expansion IS the runtime`ui! { button(label = "Hi") }` becomes `button(&ButtonProps { ... })` becomes a `Element::button { ... }` constructor. No virtual DOM diff, no JSX-to-element transformation, no reflection pass, no decorator metadata. You read the macro as one thing; the compiler reads it as another; the machine code is the second one.There's no shared runtime to interpret, no scheduler that owns your component tree, no abstraction layer the framework lazily resolves at render time. Every abstraction the macros expose collapses to a direct call against a primitive constructor.
The tradeoffRust costs something. You need the toolchain installed (rustup is one command, but it's not nothing). The first compile of a fresh project is slow — cargo builds the framework crates from scratch. And if you've never seen the language before, there's a learning curve: ownership, lifetimes, the borrow checker.The framework's macros hide most of the day-to-day friction, and the type system catches entire categories of bugs before you run the code. We think the leverage above earns the cost. The Quickstart is the fastest way to find out for yourself.Try the Quickstart →
On this page
The boilerplate, briefly
Shape of the language fits UI
Expressions, not statements
Pattern matching as render switch
Enums and invalid states
Macros over explicit code
Closures with explicit capture
Ownership for refs
Traits, not inheritance
The macro expansion IS the runtime
The tradeoff
IdealystOne codebase, native everywhere.
© Idealyst 2026