ohkami-rs
uibeam
Rust

A struct-based, JSX-style Web UI library for Rust

Last updated May 7, 2026
80
Stars
3
Forks
13
Issues
0
Stars/day
Attention Score
29
Language breakdown
No language data available.
โ–ธ Files click to expand
README

UIBeam

A struct-based, JSX-style Web UI library for Rust

License CI status crates.io

  • UI! : JSX-style template syntax with compile-time checks
  • Beam : 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.
Additionally, HTML completions and hovers in 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 add serde to 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.
(e.g. 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&lt;&#39;de&gt; Deserialize&lt;&#39;de&gt;. #[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 build with RUSTFLAGS='--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 /.uibeam route:
/ 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 openapi feature is supported via UIBeam's openapi feature 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.

๐Ÿ”— More in this category

ยฉ 2026 GitRepoTrend ยท ohkami-rs/uibeam ยท Updated daily from GitHub