Core conceptsThe ideas you need to hold in your head to read or write idealyst code: the app/host split, Element, signals, the `ui!` macro, the builder/Element distinction, the Backend trait, and the path to building your own component library and theme system on top.
App crate vs host crateYour code lives in an app crate that depends only on `runtime-core`. It declares components, styles, and a root tree, and knows nothing about the platform it will run on.A host crate is the tiny per-target wrapper that picks a backend and mounts the app. Hosts are generated by the CLI under `target/idealyst/<platform>/wrapper/` and are usually a single file. The same `app()` function compiles into each host unchanged.This separation is what makes "one codebase, every platform" mean what it says — the platform code is the host, and you don't write it.
Primitives are the vocabularyEvery UI tree is built from a fixed set of `Element` variants. The framework tells each backend "create one of these, update its props, attach these children" — the backend translates that into a UIKit view, a DOM element, a wgpu draw call, whatever it owns.// Built-in primitives in runtime-core::Element:
View · Text · Button · Image · TextInput · TextArea · ScrollView
Slider · Toggle · Icon · Link · ActivityIndicator · Graphics
Video · Virtualizer · Navigator · External (third-party SDKs)
Components are functions that return a `Element` tree. `#[component] fn Welcome(props: &Props) -> Element` is the whole contract. Composition is function composition; there's no class, no lifecycle, no special framework method to override. Signals (the reactive layer)`Signal<T>` is the framework's reactive primitive. Reads inside a reactive scope (a component body, an effect, a style closure) subscribe the surrounding scope to the signal's change set. Writes fire every subscriber once — no re-render passes, no diffing, no reconciler.let count = signal!(0);
// `text_fmt!` builds a reactive text source. `bind!(signal)`
// marks the args the framework subscribes to:
ui! { text { text_fmt!("Count: {}", bind!(count)) } }
// Update fires every subscribed dependent and nothing else:
count.update(|n| *n += 1); Effects, derived signals, and batched writes are all exposed (see `framework_core::reactive`). The model is fine-grained: only the primitives whose dependencies changed get touched. The `ui!` macro`ui!` is the declarative authoring syntax. It's a proc-macro that desugars to plain function calls against props structs and `Element` constructors. Both forms compile to the same code; the macro is opt-in (but recommended) sugar.// `ui!` is sugar over plain function calls.
ui! {
view(style = card) {
text { "Hello" }
button(label = "OK", on_click = on_ok)
}
}
// ...desugars to roughly this:
view(card, vec![
text("Hello".to_string()),
button(&ButtonProps {
label: "OK".to_string(),
on_click: on_ok,
..Default::default()
}),
]) The same author surface includes `#[component]` (turn a function into a memoizable component), `stylesheet!` (typed stylesheets with variants), `signal!` (allocate a reactive value), and `jsx!` (an alternate angle-bracket syntax for authors who prefer it). Builders and `Element`The `ui!` macro hides one detail you'll meet the moment you step outside it. Element constructors don't return `Element` directly — they return a builder wrapper, typically `Bound<H>`, where `H` is the primitive's handle type (`TextHandle`, `ButtonHandle`, etc.). The wrapper exists so you can chain configuration before committing to the final value.text(move || format!("Count: {}", count.get()))
.bind(label_ref) // attach a Ref<TextHandle>
.with_style(my_style) // apply a stylesheet
.accessibility(a11y_props) // accessibility metadata Each method takes `self` and returns `Self`, so the chain accumulates state. At the end of the chain you still have a `Bound<TextHandle>`, not a `Element`.Inside the `ui!` macro, an `.into_element()` call is inserted at every leaf so the framework's tree sees `Element` values. Outside `ui!`, you call it yourself.// Inside `ui!`, the macro inserts the unwrap for you.
// Outside `ui!`, you spell it yourself:
let label = text(move || format!("Count: {}", count.get()))
.into_element();
// `label` is now a Element — can go into a Vec<Element>
// or be passed as a child. If you forget the unwrap and try to use the builder where a `Element` is expected, the compiler tells you exactly that: `expected Element, found Bound<TextHandle>`. The fix is one method call.The same shape applies to every builder-returning function in the framework — `text`, `pressable`, `image`, `anchored_overlay`, `code_block`, and the idea-ui components that wrap them. If a function returns a builder, treat `.into_element()` as its terminator when you need the bare `Element`. The Backend traitThe framework's only seam to the platform. Implement this trait once per target and the entire existing app surface runs there. Web is `<div>`s and DOM mutations; iOS is `UIView` + `addSubview`; Android is `android.view.View` + `addView`; wgpu is GPU draw calls.// Sketch of the Backend trait (real surface is larger):
pub trait Backend {
type Node;
fn create_view(&mut self, ...) -> Self::Node;
fn create_text(&mut self, ...) -> Self::Node;
fn create_button(&mut self, ...) -> Self::Node;
fn update_text(&mut self, node: &Self::Node, ...);
fn insert(&mut self, parent: &Self::Node, child: Self::Node, idx: usize);
// ...one method per primitive + property update path
} Adding a new platform is implementing a trait, not forking the framework. Most backends fit in a single crate of moderate size. Building your ownThe framework's core is small on purpose. Most of what idealyst-the-shipping-product is — `idea-ui`, the navigator SDKs, the icon registry — sits on top of the framework as ordinary user code. Your own component library and theme system slot in the same way.Custom component librariesA component library is a crate full of `#[component]` functions. Each one takes a props struct, builds a `ui!` tree, returns a `Element`. There's nothing privileged about idea-ui's components versus yours — they all go through the same path.// A component is a function returning a Element.
// That's the whole contract — no class, no lifecycle
// method, no framework-side registration.
#[component]
pub fn MyButton(props: &MyButtonProps) -> Element {
let on_click = props.on_click.clone();
let label = props.label.clone();
let style = button_style().intent(props.intent);
ui! {
Pressable(on_click = move || (on_click)()) {
text(style = style) { label }
}
}
} Bundle a crate full of these, export them, and your library's consumers `use my_lib::my_button` like any other Rust function. The macros and primitives the framework ships are designed to be composed; there's no framework-side registration step, no metadata to ship, no plugin manifest.Custom theme systemsA theme is a collection of style tokens. Your library defines a struct that implements `ThemeTokens` — its `tokens()` method returns the `(name, value)` table to install — and stylesheets reference those values by token name via `Tokenized::token("…", fallback)`, never by reading the struct in their bodies. Token reads are reactive: a dark/light swap (`set_theme(..)`) updates the tokens and re-applies exactly the rules that read a changed one, so the same component code adapts to whatever theme is currently installed.// 1. A theme is a struct that produces a flat token table.
// `ThemeTokens::tokens()` returns the (name, value) pairs.
impl ThemeTokens for MyTheme {
fn tokens(&self) -> Vec<TokenEntry> {
vec![
TokenEntry { name: "color-accent", value: TokenValue::Color(self.accent.clone()) },
TokenEntry { name: "radius-md", value: TokenValue::Length(self.radius) },
]
}
}
// 2. Stylesheets reference values by token NAME — not by reading
// a theme struct. The `<()>` / `(_t)` slots are vestigial.
stylesheet! {
pub MyButton<()> {
base(_t) {
background: Tokenized::token("color-accent", Color("#5b6cff".into())),
border_radius: Tokenized::token("radius-md", Length::Px(8.0)),
}
}
}
// 3. Install at bootstrap; swap any time with set_theme(..).
install_theme(MyTheme::default()); The pattern is straight from `idea-ui`. Its theme structs implement `ThemeTokens`; `light_theme()` / `dark_theme()` are ready-made instances the app installs, and every shipped stylesheet references the resulting tokens by name. Apps that want to retheme implement `ThemeTokens` on their own struct — same path, just more tokens.Read idea-ui's source as the canonical reference. It's an entire component library + theme system written against the framework's primitives — nothing more privileged than what your own code can do. Where to go from hereIf you want the language-shape argument — why this is written in Rust rather than TypeScript or Kotlin — read the Why Rust page.Why Rust →If you want the per-target implementation status — which primitives are wired on which backends — the Backends page has the matrix.Backends →