AkurAI Build
Menu

Public repository / a0501370bec01f30f21f48fbebd95284b81aca77

AkurAI-Framework / site/content/docs/rust-ui.md

Latest change 3aae9f4e85241bcb775c31f5c03094becd3da56a - Pure-Rust UI layer: akurai-ui, akurai-crypto, Tailwind-grade akurai-css by Ólafur Búi Ólafsson

# Rust UI: `akurai-ui`

`akurai-ui` is the framework's pure-Rust front end: React-style components,
fine-grained signals, and an accessible component kit in the spirit of Radix UI
and shadcn/ui, compiled to WebAssembly. Like the rest of the stack it links
**no external crates** — no `wasm-bindgen`, no `web-sys`. The browser side is
`host.js`, a ~150-line shim that owns a table of DOM nodes and forwards events,
fetches and timers back into Rust.

Use it when a page is an *application* (a planner, a dashboard, an editor) and
you would otherwise reach for React. For mostly-static pages, the
[reactivity](/docs/reactivity) directives and [islands](/docs/islands) remain
lighter.

## Components are functions

A component is a function returning something `IntoView`: an element, text, a
tuple or `Vec` of views, or a closure. Elements are built with a chainable API
that reads top-down like JSX:

```rust
use akurai_ui::prelude::*;
use akurai_ui::kit::{self, Size, Variant};

fn counter() -> impl IntoView {
    let count = signal(0);
    div().class("flex items-center gap-2").child((
        kit::button(Variant::Primary, Size::Md)
            .on_click(move |_| count.update(|c| *c += 1))
            .child("+1"),
        span().child(move || count.get().to_string()),
    ))
}

akurai_ui::app!(counter);
```

## Signals, effects, scopes

`signal(v)` is a reactive cell (`get`, `set`, `update`, `with`). A closure
used as a child or attribute value is a *dynamic region*: it re-runs when a
signal it read changes and patches only its own DOM. `memo` derives values that
notify only on change; `batch` coalesces writes. Every signal, effect and event
listener belongs to a scope, so content that leaves the screen (a closed
dialog, a removed list row) releases everything it created.

- `show(when, then, otherwise)` / `when(cond, then)` — conditional regions.
- `each(items, key, row)` — keyed lists; rows keep their DOM and state across
  updates and receive their item as a `Signal`.
- `portal(view)` — mount at the end of `<body>` (dialogs, toasts).
- `provide_context` / `use_context` — dependency injection down the tree.
  Event handlers and fetch callbacks run in the scope that registered them,
  so they see the same context.

## The kit

`akurai_ui::kit` ships `button`, `icon_button`, `card*`, `input`, `textarea`,
`label`, `field` (label + hint wired with ARIA), `select`, `checkbox` and
`switch` (Radix semantics: `role`, `aria-checked`, `data-state`), `tabs`
(roving focus, arrow keys, Home/End), `dialog` and `confirm_dialog` (portalled,
labelled, Escape/overlay close, focus moved in, trapped and restored),
toasts (`provide_toaster`, `toast`, `toast_error`, an `aria-live` region),
`badge`, `alert`, `avatar`, `separator`, `skeleton`, `spinner`, `empty_state`
and Lucide-style stroke `icons`. Corners follow the house rule: nothing rounder
than 6px.

## Styling

Components use utility classes from [`akurai-css`](/docs/styling) and the theme
tokens (`bg`, `panel`, `fg`, `muted`, `accent`, …). Build the stylesheet with
`akurai_css::build_any`, scanning your `.rs` sources **plus**
`akurai_ui::STYLE_SOURCES` (the kit's own classes), and prepend
`akurai_css::theme::themes_css()` and `akurai_ui::BASE_CSS`. `akurai serve`
also scans `.rs` files when it generates `/utilities.css`.

## Testing without a browser

Off-wasm, the same host API is served by an in-memory DOM, so components and
whole apps are tested with `cargo test`:

```rust
use akurai_ui::testing as t;

t::render(app);
t::respond("/api/me", 200, r#"{"name":"Ada"}"#);   // answer the app's fetch
t::click(t::get("button:text(Sign in)"));
t::input(t::get("[role=dialog] input"), "Swim practice");
assert!(t::body_html().contains("Swim practice"));
```

Selectors support `tag`, `#id`, `.class`, `[attr]`, `[attr=value]`,
`:text(Exact text)` and descendant combinators. Timers run with
`run_timers()`, and `set_hash` drives the router.

## Shipping

Build the app crate (`crate-type = ["cdylib", "rlib"]`) for
`wasm32-unknown-unknown`, serve the `.wasm` next to `akurai_ui::HOST_JS`, and
boot it:

```html
<script type="module">
  import { boot } from "/akurai-ui.js";
  boot("/app.wasm");
</script>
```

The family planner (`~/Projects/family`) is the reference app.