Stop shipping JavaScript.* except on the server, obviously.
Build interactive Datastar pages from plain TypeScript: typed data-* attributes, server-rendered TSX, signal readers, and native Response helpers.
import { Hono } from "hono"
import { post, reply } from "datastar-kit"
const app = new Hono()
let count = 0
const Count = () => <output id="count">{count}</output>
app.get("/", () =>
reply.page(
<main>
<button data-on:click={post("/increment")}>Increment</button>
<Count />
</main>,
{ title: "Counter" }
)
)
app.post("/increment", () => {
count += 1
return reply.patch(<Count />)
})
export default appRuns where you do.
Bring any Fetch-compatible runtime.
One loop, owned by your server.
The browser handles events and patches. Your code keeps the application model.
01 · Render
Serve the first view as HTML from backend state.
02 · Listen
Attach Datastar attributes to ordinary elements.
03 · Handle
Decode signals and run normal TypeScript logic.
04 · Patch
Return HTML or signals as native Response objects.
Live · Durable Object
A counter the whole internet shares.
Tap it. The count lives in a Cloudflare Durable Object and streams to every open tab over SSE — yours and everyone else's. Open a second tab and watch it move.
A kit, not a framework.
Datastar Kit owns the Datastar-shaped pieces and nothing else. Everything works anywhere a Request becomes a Response.
const todo = state({ title: "" })
const Composer = () => (
<form
data-signals={mod(todo.defaults, { ifMissing: true })}
data-on:submit={mod(post("/todos"), { prevent: true })}
>
<input data-bind={todo.refs.title} />
<button type="submit">Add</button>
</form>
)
app.post("/todos", async (c) => {
const { title } = TodoSignals.parse(await read.signals(c.req.raw))
await db.todos.insert(title)
return reply.stream([
event.patch(<TodoList items={await db.todos.list()} />),
event.signals(todo.reset())
])
})- Typed attributes and actions
- data-on, data-bind, data-show, and friends are real TSX props.
- Response helpers
- reply.page, reply.patch, reply.signals, reply.stream, and reply.navigate return native Responses.
- Signals at the boundary
- read.signals(request) decodes Datastar payloads for your own validation layer.
- Realtime in a few lines
- reply.stream sends SSE patches, so live multi-tab views are an ordinary handler.
- A development debugger
- Drop one component into a page to inspect signals, patches, and SSE traffic.
Readable in an afternoon.
Install it, read the introduction, and ship one server-driven page.