Pulse-native JSX framework for direct-DOM applications.
Fine-grained rendering with explicit routing, async primitives, and SSR.
Start a new app with the scaffolder:
pnpm dlx @ochairo/beat-create my-appUse the showcases template for a full-featured app with routing, crypto dashboard, kanban board, and spreadsheet:
pnpm dlx @ochairo/beat-create my-app --template showcasesThat command scaffolds a Vite + TypeScript starter already configured for Beat's JSX runtime and Vite plugin.
import { component } from "@ochairo/beat";
import { pulse } from "@ochairo/pulse";
const counter = pulse(0);
const onclick = (value) => {
counter.set(counter.get() + value);
};
export const App = component(() => {
return (
<main>
<header class="header">
<h1 class="title">Counter app</h1>
</header>
<section class="counter">
<button class="button" onClick={() => onclick(-1)}>
-
</button>
<strong class="result">{counter}</strong>
<button class="button" onClick={() => onclick(1)}>
+
</button>
</section>
</main>
);
});Beat's SSR uses the same component tree and router — two entry points, no new framework.
// entry-server.ts
import { Window } from "happy-dom";
import { createRouter } from "@ochairo/beat";
import { renderToString, waitForRouter } from "@ochairo/beat/server";
export async function render(url: string): Promise<string> {
const win = new Window({ url });
globalThis.document = win.document as unknown as Document;
const router = createRouter({ routes, initialUrl: url });
await waitForRouter(router, { signal: AbortSignal.timeout(5_000) });
const html = renderToString(() => <App router={router} />);
win.happyDOM.close();
return `<div id="app">${html}</div>`;
}
// entry-client.ts
import { createRouter, hydrate } from "@ochairo/beat";
const router = createRouter({ routes, window });
hydrate(document.getElementById("app")!, <App router={router} />);initialUrl— resolves the route on the server without readingwindow.locationwaitForRouter— waits for route loaders to settle before rendering; acceptsAbortSignalrenderToString— takes a factory() => JSX; suppressesonMounton the serverhydrate— single atomic swap from server HTML to live Beat tree, no blank frame