A web framework on axum
cargo build builds it, with no Node and no wasm, and a mismatch between the page and the server is a compile error.
cargo build builds it, with no Node and no wasm. The crate embeds a prebuilt runtime of about 14 KB gzipped.
Signal names, expressions, actions, and links come from Rust types. A link whose page has no handler, or whose path parameters or query do not fit the page, is a compile error, as is any other mismatch between the page and the server.
A statement changes the page in the browser, and an action call sent with it reaches the server after. Changing the page before the server answers takes no extra code.
hypersig is the framework. Three crates in the same repository add typed styles, a set of accessible components, and typed translations, each with docs of its own.
hypersig_styleTyped atomic CSS: every property a method and every keyword a variant, with tokens, themes, and conditions. Reached as hypersig::style.
hypersig_uiAccessible components styled after shadcn/ui, built on native elements and following the WAI-ARIA Authoring Practices.
Explore Components →hypersig_i18nTyped translations: a text without one of its translations fails to compile, and the language is part of each page's route.
Explore i18n →The signal struct holds the page's state. Counter::count() is a typed reference to its field, and count.set(count + 1) is a statement the browser runs on a click without asking the server. The server renders the page with the count already filled in, so the page shows before the runtime loads.
Result
use hypersig::html::{div, span};
use hypersig::style::Style;
use hypersig::style::pattern::hstack;
use hypersig::style::value::{FontWeight, TextAlign, rem};
use hypersig::{Page, Signals};
use hypersig_ui::{ButtonSize, ButtonVariant, button};
use serde::{Deserialize, Serialize};
#[derive(Signals, Serialize, Deserialize)]
pub struct Counter {
count: i32,
}
pub fn page() -> Page<Counter> {
let count = Counter::count();
Page::new(
"Counter",
Counter { count: 0 },
)
.child(
div()
.css(hstack(rem(1)))
.child(
button(ButtonVariant::Outline, ButtonSize::Default)
.on_click(count.set(count - 1))
.text("-1"),
)
.child(
span()
.css(
Style::new()
.min_width(rem(3))
.text_align(TextAlign::Center)
.font_size(rem(1.5))
.font_weight(FontWeight::Number(600.0)),
)
.bind_text(count.to_text()),
)
.child(
button(ButtonVariant::Default, ButtonSize::Default)
.on_click(count.set(count + 1))
.text("+1"),
),
)
}Topcoat and Datastar come closest to hypersig. Both keep signals in the browser and update the page from the server with no browser build, and Topcoat writes the browser's expressions in Rust too.
| Topcoat and Datastar | hypersig | |
|---|---|---|
| Signal names and URLs (Datastar) | Strings in HTML attributes. A typo shows up when the browser runs it | Written from Rust values. A typo stops cargo build |
| Link parameters (Topcoat) | href! checks only the handler. A missing or extra path parameter panics when the page renders, and the request answers 500 | Route takes its parameters as a typed value. A wrong one is a compile error |
| Adding to a list (Topcoat) | Expressions cannot add an item, so the list changes once the server answers and renders it again | The list is a signal, so the browser shows the change before the server answers |
| hypersig | Topcoat | React | Next.js | TanStack Start | Nuxt | Datastar | |
|---|---|---|---|---|---|---|---|
| Page written in | Rust | Rust | TypeScript | TypeScript | TypeScript | TypeScript and Vue templates | HTML attributes |
| Server written in | Rust | Rust | Any | TypeScript | TypeScript | TypeScript | Any |
| No browser build | Yes | Yes | No (Vite) | No (Node bundler) | No (Vite) | No (Vite) | Yes |
| Request types checked at compile time | Yes | Yes | No | Yes | Yes | No | No |
| Links checked against routes at compile time | Yes | Handler only (parameters at run time) | No (no router) | Opt-in (typedRoutes) | Yes | Experimental (typedPages) | No |
| State kept in the browser | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
A 2000-item TodoMVC that stores every add, toggle, and delete on the server. Medians of 3 runs in Chrome 155 on an Apple M4 Max, with the CPU slowed 4x and 50 ms added to every request. The apps and the harness are in bench/compare.
| hypersig | Topcoat | React | Next.js | TanStack Start | Nuxt | Datastar | |
|---|---|---|---|---|---|---|---|
| JS (gzip) | 19.2 KB | 14.8 KB | 67.1 KB | 133.3 KB | 119.5 KB | 59.2 KB | 13.1 KB |
| First Contentful Paint | 186 ms | 165 ms | 196 ms | 198 ms | 193 ms | 177 ms | 182 ms |
| Total Blocking Time | 0 ms | 313 ms | 5 ms | 10 ms | 0 ms | 29 ms | 16 ms |
| Interaction to Next Paint | 24 ms | 24 ms | 24 ms | 24 ms | 24 ms | 16 ms | 16 ms |
The same page styled with hypersig::style, Tailwind CSS, Panda CSS, StyleX, and vanilla-extract is measured on the Styling page.