Open source · MIT or Apache-2.0

Native apps in TSX, rendered by real native views.

Write components in TSX. nsh runs them in QuickJS on a runtime written in Rust — reactive graph, renderer and layout included — and draws every element with UIKit or Android views. No web view, no Node.js, one binary.

nsh init my-app
nsh run my-app/App.tsx
  • iOS 15+ · Android · Mac Catalyst
  • One Rust binary
  • No Node.js
Running live · counter.tsxOpen ↗
One file

This is the whole app.

The phone above runs this source. Your app’s code is the only JavaScript; everything it calls into is native.

signal()
State lives in a native reactive graph. A change updates exactly the text and props that read it — no virtual DOM diff.
<view> · <text>
Host elements map to UIView and android.view.View, so touch, accessibility and the keyboard are the system’s own.
style={{ … }}
React Native style semantics, laid out with flexbox and grid by Taffy on the engine thread.
nsh run
Starts a dev server and the app; every save hot-reloads only the edited components and keeps the rest of the state.
demos/counter.tsx
import { mount, signal } from "nsh";

function Button(props: { label: string; color: string; onPress: () => void }) {
  return (
    <view
      onPress={props.onPress}
      style={{
        backgroundColor: props.color,
        borderRadius: 12,
        paddingVertical: 12,
        paddingHorizontal: 24,
        alignItems: "center",
      }}
    >
      <text style={{ color: "#ffffff", fontSize: 18, fontWeight: "bold" }}>{props.label}</text>
    </view>
  );
}

function App() {
  const count = signal(0);
  const history = signal<number[]>([]);

  const change = (delta: number) => {
    const next = count() + delta;
    count.set(next);
    history.update((h) => [next, ...h].slice(0, 5));
  };

  return (
    <view style={{ flex: 1, backgroundColor: "#f4f5f7", padding: 24, paddingTop: 64, gap: 24 }}>
      <text style={{ fontSize: 28, fontWeight: "bold", color: "#1f2328" }}>nsh counter</text>
      <view
        style={{
          backgroundColor: "#ffffff",
          borderRadius: 16,
          padding: 24,
          alignItems: "center",
          gap: 8,
        }}
      >
        <text style={{ fontSize: 14, color: "#6e7781" }}>Current value</text>
        <text style={{ fontSize: 64, fontWeight: "bold", color: count() < 0 ? "#cf222e" : "#1f2328" }}>
          {count()}
        </text>
      </view>
      <view style={{ flexDirection: "row", gap: 12, justifyContent: "center" }}>
        <Button label="−1" color="#cf222e" onPress={() => change(-1)} />
        <Button label="+1" color="#1f883d" onPress={() => change(1)} />
      </view>
      <view style={{ gap: 6 }}>
        <text style={{ fontSize: 14, color: "#6e7781" }}>
          Recent values: <text style={{ fontWeight: "bold", color: "#1f2328" }}>{history().length}</text>
        </text>
        <each of={history()}>{(value) => <text style={{ fontSize: 16, color: "#1f2328" }}>• {value()}</text>}</each>
      </view>
    </view>
  );
}

mount(App);
Capabilities

A native runtime, not a bridge.

Each of these is something you can check in the repository: the crate, element or command is named on the card.

QuickJS on a native runtime

Only your app’s code is JavaScript. The reactive graph, the renderer and every module you import are Rust; compiled JSX calls them directly.

  • QuickJS
  • nsh:rt

Signals

signal, computed and effect: fine-grained reactivity scheduled natively. A change re-renders only what reads it.

  • signal()
  • computed()
  • effect()

Flexbox and grid

Taffy lays out every node on the engine thread, with React Native’s style semantics plus CSS grid.

  • Taffy
  • flex
  • grid

Tailwind, resolved natively

class="p-4 rounded-xl" becomes styles in Rust; unknown classes are reported at build time, and themes live in nsh.json.

  • class
  • nsh.json

Real native views

Every element is a UIView or android.view.View, so maps, web views and video embed with no layering tricks.

  • UIKit
  • Android View

Animations on the UI thread

Timing, spring and decay drivers run in a native animation graph. JS starts them and is told when they settle.

  • nsh/animated
  • spring

Virtualized List

Recycled cells, columns, sticky section headers and inverted chat lists, with cells bound natively while you scroll.

  • <list>
  • sticky
  • inverted

One binary toolchain

nsh bundles with Rolldown and Oxc, writes type declarations, serves dev builds and packages apps. No node_modules.

  • nsh init
  • nsh run
  • nsh package

Hot module replacement

Save a file and only the edited components render again; the rest of the app keeps its state. Logs stream to your terminal.

  • nsh dev
  • HMR

Signed packages

Ship updates as .nshpkg: Ed25519-signed, release keys certified by an offline root, revocation lists checked on device.

  • nsh publish
  • nsh verify

Embeddable

Put an AppView or NshView into an existing iOS or Android app — several instances, no global singletons — and register your own native components.

  • Swift
  • Java

Components written in Rust

Button, Card, Sheet and the rest of nsh/ui/mobile are Rust, run in the core and look the same on iOS and Android.

  • nsh/ui/mobile
Architecture

One commit per frame, one hop across threads.

Everything one JS turn changes is queued natively, applied in a single Core::commit, and handed to the main thread as one MountBatch. The main thread never waits for JS.

The main thread never waits for JS

Callbacks the system needs answered on the spot — may this text change, may the back button pop — are answered natively from props mounted beforehand; JS hears about it afterwards.

Threads are crossed in batches

No per-item FFI calls. Node ids are dense indexes, and unchanged styles and frames are never sent.

Text is laid out once

CoreText or StaticLayout measures and lays out text on the engine thread; the main thread only draws the result.