A struct-based, JSX-style Web UI library for Rust
UIBeam
A struct-based, JSX-style Web UI library for Rust
UI!: JSX-style template syntax with compile-time checksBeam: Component system based on Rust structs
Features
- Struct-based component model.
- Client components via island architecture in Wasm. (experimental) (See Client Component section below)
- Best effort to generate efficient code for template rendering with less redundant memory allocation.
UI! by VSCode extension. (searchuibeam in the extension marketplace)

Usage
[dependencies]
uibeam = "0.4"
When using uibeam just as a template engine, disabling client default feature is recommended to eliminate useless dependencies:
[dependencies]
uibeam = { version = "0.4", default-features = false }
UI! syntax
use uibeam::UI;
fn main() { let user_name = "foo";
let style = " color: red; \ font-size: 20px; \ "; let ui: UI = UI! { <p class="hello" style={style}> "Welcome to the world of UIBeam!" <br> "ใใใซใกใฏ" <a class="user" style="color: blue;" data-user-id="123" href="https://example-chatapp.com/users/123" > "@"{user_name}"!" </a> </p> };
println!("{}", uibeam::shoot(ui)); }
unsafely insert HTML string
raw string literal (r#"..."#) or unsafe block contents are rendered without HTML-escape :
,ignore use uibeam::UI;
fn main() { println!("{}", uibeam::shoot(UI! { <html> <body> / โ wrong here: scripts are html-escaped... /
/ โ scripts are NOT html-escaped, rendered as they are /
</body> </html> })); }
conditional & iterative rendering
{} at node-position in UI! can render, in addition to Display-able values, any impl IntoIterator<Item = UI>. This includes Option<UI> or any other iterators yielding UIs !
use uibeam::{UI, Beam};
struct Task { id: u64, title: String, subtasks: Vec<String>, completed: bool, }
fn main() { let t = Task { id: 42, title: "try uibeam".to_string(), subtasks: vec![], completed: false, };
let ui = UI! { <div id={format!("task-{}", t.id)}> <h2>{t.title}</h2>
<h3>"subtasks"</h3> <ul> {t.subtasks.iter().map(|s| UI! { <li>{s}</li> })} </ul>
{t.completed.then_some(UI! { <i><strong>"completed"</strong></i> })} </div> };
println!("{}", uibeam::shoot(ui)); }
Beam - Component with Rust struct and JSX-like syntax
use uibeam::{Beam, UI};
struct Layout { title: String, children: UI, // children field }
impl Beam for Layout { fn render(self) -> UI { UI! { <html> <head> <title>{self.title}</title> <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css"> </head> <body class="bg-gray-100"> {self.children} </body> </html> } } }
struct AdminPage {}
impl Beam for AdminPage { fn render(self) -> UI { UI! { <main class="container mx-auto flex-grow py-8 px-4"> <section class="bg-white shadow-md rounded-lg p-6"> <h1 class="text-2xl font-bold text-gray-800 mb-6"> "Password" </h1> <form method="post" action="" class="w-full"> <div class="flex flex-col gap-4"> <div class="flex flex-col"> <label for="adminPassword" class="text-gray-700 text-sm font-bold mb-1"> "password" </label> <input required type="password" id="adminPassword" name="adminPassword" class="py-2 px-3 border border-gray-400 rounded focus:outline-none focus:shadow-outline" /> </div> </div> <div class="mt-6"> <button type="submit" class="bg-purple-500 hover:bg-purple-700 text-white py-2 px-4 rounded focus:outline-none focus:shadow-outline" > "Send" </button> </div> </form> </section> </main> } } }
fn main() { let ui = UI! { <Layout title="admin page"> // title: ("admin page").into() <AdminPage /> // children: (AdminPage {}).render() </Layout> };
println!("{}", uibeam::shoot(ui)); }
Client Component - Wasm islands
overview
#[client] makes your Beam a Wasm island : initially rendered on server, sent with serialized props, and hydrated with deserialized props on browser.
Signal, computed, effect, batch, untracked are available in them.
note
EXPERIMENTAL.
Currently UIBeam's hydration/reactivity system is built upon Preact. This will be rewritten in pure Rust in the future.
usage
working example: examples/counter
- Activate
"client"feature, and addserdeto your dependencies:
[dependencies]
uibeam = { version = "0.4" } # client is a default feature
serde = { version = "1", features = ["derive"] }
- Configure to export all your client components from a specific library crate.
lib.rs entrypoint, or another member crate of a workspace)
(There's no problem if also including ordinary Beams in the lib crate.)
Additionally, specify crate-type = ["cdylib", "rlib"] for the crate:
[lib]
crate-type = ["cdylib", "rlib"]
- Define and use your client components:
/ islands/src/lib.rs /
use uibeam::{UI, Beam};
use uibeam::{Signal, callback, client::PointerEvent};
use serde::{Serialize, Deserialize};
struct CounterButton {
on_click: Box<dyn Fn(PointerEvent)>,
children: UI,
/// additional classes to modify default style
class: Option<&'static str>,
}
#[uibeam::client] // client component, but not Serialize/Deserialize and not at island boundary
impl Beam for CounterButton {
fn render(self) -> UI {
UI! {
<button
class={self.class.unwrap_or("")}
onclick={self.on_click}
>
{self.children}
</button>
}
}
}
// client component at island boundary must be Serialize + for<'de> Deserialize<'de>. #[derive(serde::Serialize, serde::Deserialize)] pub struct Counter { pub initial_count: i32, } // (island) means island boundary #[uibeam::client(island)] impl Beam for Counter { fn render(self) -> UI { let count = Signal::new(self.initial_count);
// callback! - a thin utility for callbacks using signals. let increment = callback!( // [dependent signals, ...] [count], // closure depending on the signals |_| count.set(*count + 1) ); /* << expanded >> let increment = { let count = count.clone(); move |_| count.set(*count + 1) }; */
let decrement = callback!([count], |_| { count.set(*count - 1); });
UI! { <div> <p> "Count: "{*count} </p> <div> <CounterButton on_click={Box::new(decrement)} class={None} >"-"</CounterButton> <CounterButton on_click={Box::new(increment)} class={None} >"+"</CounterButton> </div> </div> } } }
,ignore
/ server/src/main.rs /
use islands::Counter;
use uibeam::UI;
async fn index() -> UI {
UI! {
<Counter />
}
}
NOTE:
Client Beam at island boundary must be Serialize + for<'de> Deserialize<'de> for the Wasm island architecture.
In contrast, #[client] component that, e.g. has children: UI or on_something: Box<dyn FnOnce(Event)>
as its props, can NOT implement Serialize nor Deserialize, can NOT has (island),
and can only be used internally in UI! of another client component.
- Compile the lib crate into Wasm by
wasm-pack buildwithRUSTFLAGS='--cfg hydrate'and--out-name hydrate --target web:
# example when naming the lib crate islands
cd islands RUSTFLAGS='--cfg hydrate' wasm-pack build --out-name 'hydrate' --target web
# in a hot-reloading loop, --dev flag is recommended:
cd islands RUSTFLAGS='--cfg hydrate' wasm-pack build --out-name 'hydrate' --target web --dev
NOTE: Both hydrate cfg (not feature!) and hydrate out-name are required here. This restriction may be relaxted in future versions.
- Setup your server to serve the output directory (default:
pkg) at/.uibeamroute:
/ axum example /
use axum::Router; use tower_http::services::ServeDir;
fn app() -> Router { Router::new() .nest_service( "/.uibeam", ServeDir::new("./islands/pkg") ) // ... }
(as a result, the generated {crate name}/pkg/hydrate.js is served at /.uibeam/hydrate.js route, which is automatically loaded together with corresponding .wasm file in the hydration step on browser.) NOTE: Make sure that your server responds with a complete HTML consist of one <html></html> containing your page contents.
Integrations with web frameworks
Enables UI to be returned directly as a HTML response.
Axum - by "axum" feature
axum = { version = "0.8" }
uibeam = { version = "0.4", features = ["axum"] }
,no_run
use axum::{routing::get, Router};
use uibeam::UI;
async fn handler() -> UI { UI! { <h1>"Hello, Axum!"</h1> } }
#[tokio::main] async fn main() { let app = Router::new() .route("/", get(handler));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); axum::serve(listener, app).await.unwrap(); }
Actix Web - by "actix-web" feature
actix-web = { version = "4.12" }
uibeam = { version = "0.4", features = ["actix-web"] }
,no_run
use actix_web::{HttpServer, App, get};
use uibeam::UI;
#[get("/")] async fn handler() -> UI { UI! { <h1>"Hello, Actix Web!"</h1> } }
#[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| App::new() .service(handler) ) .bind(("127.0.0.1", 8080))? .run() .await }
Ohkami - by "ohkami" feature
- UIBeam v0.4 is compatible with Ohkami v0.24.
- Ohkami's
openapifeature is supported via UIBeam'sopenapifeature flag. - UIBeam itself is runtime-agnostic and available with any async runtimes supported by Ohkami.
[dependencies]
tokio = { version = "1.48", features = ["full"] }
ohkami = { version = "0.24", features = ["rt_tokio"] }
uibeam = { version = "0.4", features = ["ohkami"] }
when using ohkami's "openapi" feature,
activate also uibeam's "openapi" feature.
,no_run
use ohkami::{Ohkami, Route};
use uibeam::UI;
async fn handler() -> UI { UI! { <h1>"Hello, Ohkami!"</h1> } }
#[tokio::main] async fn main() { Ohkami::new(( "/".GET(handler), )) .howl("localhost:5000") .await }
License
UIBeam is licensed under MIT LICENSE.