Ship real HTML.
Hydrate on purpose.

Binsby is a server-first React framework. Pages are React components that run on the server. Anything interactive is an island — it's the only JS your visitors download.

$pnpm create binsby@latest
01 / WHY

Most React frameworks have the defaults backwards.

They render on the server, then re-run the whole app on the client to "hydrate" it. Binsby flips it: pages ship as HTML, and the only JS that reaches the browser is the components you marked interactive — loaded when they actually need to wake up. The bundle stays honest on day one, day three hundred, and after the cart team adds another widget.

01 / The filename is the boundary

No 'use client' to argue with

.server.tsx runs on the server. .island.tsx hydrates in the browser. .action.ts runs on the server, called like a function. No directives, no RSC graph, no compiler tricks — read the extension, you know where the code runs.

02 / Hydrate on a trigger

Islands wake up when they earn it

Pick when each island boots: visible, idle, media, or first interaction. A modal sleeps until you open it. A toast handler doesn't load until someone fires it. Each island is its own chunk — pages don't share a hydration budget.

03 / Mutations as function calls

Server actions, not endpoints

Write a typed handler in a .action.ts file. Import it in an island, await it, get the result. The Vite plugin swaps in a fetch stub with CSRF, validation, and end-to-end types. You write the business logic — and nothing else.

04 / A page is an async function

Skip the data layer

Hit the database, call your CMS, read cookies — then return JSX. No loaders, no internal API for your own pages, no /api route to maintain alongside every screen, no waterfall to debug.

02 / FEATURES

Everything you need. Nothing you don't.

</>

Zero JS by default

Pages render on the server and arrive as HTML. Your hero doesn't wait on a hydration pass that isn't doing anything.

Interactive where it counts

Mark a component an island and it ships its JS — and only its JS. Static content stays static, forever.

JS only when it earns it

visible · idle · media · interaction. A toast handler doesn't load until someone fires it. A modal sleeps until you open it.

Mutations without an API

Write a server function, call it like a function from the browser. Types, validation, and CSRF are handled — you write the business logic.

REST endpoints in one file

Need a webhook target or a JSON feed for mobile? Drop a file under /api, get a typed route. Dynamic params and OpenAPI included.

Different shell per route

Marketing pages, an authed dashboard, and a checkout flow can each get their own layout. Match by URL, stack as needed.

SPA feel, MPA bones

Link clicks fetch HTML and swap the DOM. No router state to sync, no bundle to keep alive — just navigation that feels instant.

Tailwind, but not required

Tailwind v4 wired up out of the box, with @theme tokens. Prefer Sass or plain CSS? Binsby doesn't care.

Types end-to-end

Action input and output, page data, layout props — all typed, all inferred. No codegen, no runtime drift.

03 / CODE

Page, island, action. The three things you'll write.

runs on the server
1// A page is an async function. Fetch on the server, render on the server.
2import { Island } from "@bgunnarsson/binsby-islands/server"
3import { db } from "~/lib/db.server"
4
5export default async function Product({ slug }: { slug: string }) {
6  const product = await db.product.findBySlug(slug)
7
8  return (
9    <article>
10     <h1>{product.name}</h1>
11     <p>{product.description}</p>
12
13     {/* the only piece of this page that ships JS */}
14     <Island name="AddToCart" props={{ id: product.id }} when={{ type: 'visible' }} />
15   </article>
16 )
17}

// No "use client", no "use server". The filename ending tells Binsby where the code runs.

04 / FLOW

From request to interactive — what your visitor actually downloads.

◇ on the server
// your async page function
const product = await db.find(slug)
<article>
  <h1>{product.name}</h1>
  <Island name="AddToCart"/>
</article>
→ what the browser receives
<!doctype html>
<article>
  <h1>Acme Headphones</h1>
  ↯ island name="AddToCart"
  <button>Add to cart</button>
  ↯ /island
</article>
// real HTML — visible without JS
↯ when the island is in view
// when={ type: 'visible' }
load AddToCart.island
  → hydrate & wire onClick
↯ AddToCart · interactive
// idle / media / interaction also work
// other islands wait for their trigger
step 1 / 3 · page renders
05 / LIVE

One island wakes up. The rest stays HTML.

preview · what your visitor sees Product.server.tsx
<article>
  <h1>Acme Headphones</h1>
  <p>Open-back · 32Ω · made in Reykjavík</p>
↯ island · AddToCart · waking up…
0
  <p>Free shipping over $100.</p>
</article>
JS shipped for this page
Islands awake 1 / 3
First byte 14 ms
Cart quantity 0
islands on this page
AddToCart↯ visible · awake
RecommendedRail— waits for idle
ReviewForm— waits for click

// try +/−. the recommended rail and review form
// never download until they're actually needed.

06 / COMPARISON

How it differs.

Binsby Next.js Remix
Default JS per pageWhat an empty page costs 0 KB ~80 KB ~70 KB
Interactive bitsHow you opt in to client JS Tag a component 'use client' directives Everything hydrates
Picking when JS loadsvisible / idle / media / on click Per component On page load On page load
Forms & mutationsCalling server code from the browser Typed actions Server actions loader/action
API endpointsDrop a file, get a route Yes Yes Resource routes
Smooth navigationNo full reload between pages DOM swap Streaming nav Client nav
BuildWhat runs your dev server Vite Turbopack/webpack Vite
Mental modelWhat you have to learn React + 3 file types RSC + boundaries loader/action API

// ● yes · ◐ partial / opt-in · ○ no — comparison written by Binsby; mileage may vary

◇ ship something small

Build pages that don't ship a megabyte of JS.

The starter gives you a working app: pages, layouts, an API, a sign-in flow, and Tailwind ready to go. About thirty seconds to pnpm dev.

pnpm create binsby Read the docs →