Build full-stack React apps that render inside console.log.
This page is running in your console. Open it with ⌘⌥JCtrl+Shift+J⌘⌥KCtrl+Shift+K⌘⌥C (turn on developer features first), click the preview below and press ↑ ↓.
1import { useState } from "react";2import { AsciiFont, Box, Text, useInput } from "@textjs/app";34export default function Page() {5 const [count, setCount] = useState(0);6 useInput((e) => {7 if (e.key === "up") setCount((c) => c + 1);8 if (e.key === "down") setCount((c) => c - 1);9 });1011 return (12 <Box className="flex-col gap-1 p-2">13 <AsciiFont text="▼ TEXT.JS" />14 <Text color="muted">React 19 in console.log</Text>15 <Text bold color="accent">count = {count}</Text>16 </Box>17 );18}React 19
Text.js supports concurrent rendering, Suspense and error boundaries.
App Router
Folders in app/ become routes and layouts, and Back, Forward and deep links work.
Responsive grid
The grid fits the docked DevTools panel at any page zoom.
Keyboard input
Keys, paste and IME composition reach the app through one hidden <textarea>.
Tables, charts, forms and shortcuts
Every changed frame is one console.log call
A commit that changes no cells never reaches the console.
React 19
<Box> ├ <Text> └ <Text> 1
01Commit
React commits through a custom reconciler.
02Layout
Yoga lays out boxes on whole cells.
03Paint
Only damaged rows repaint.
Row hash
9f1c08e2a07e5b3131b2c4d9e8d046fa0 calls · 0 B
04Dirty gate
Unchanged frames stop here.
05Encode
3,200 cells become 149
%cspans.06Emit
Frames are appended at least 66ms apart.
Changed frame Unchanged frame, 0 calls
Appended frames
Clearing the console before each frame leaves it blank until DevTools paints the next one. Turbolog appends each frame instead.
Read the postclear() + log()70.6% blank
log(), appended1.0% blank
0.163msFull repaint (layout, paint, encode)
0.011msOne-row edit, such as a keystroke
0 callsIdle frame
149 spans%c arguments for 3,200 cells
Render routes on the server
Export runtime = "server" to render a route and its async components on the server. The browser loads a 21 kB (gzip) frame player instead of your app.
1import { Suspense } from "react";2import { Box, Text } from "@textjs/app";3import { db } from "@/lib/db";45export const runtime = "server";67async function Deploys() {8 const rows = await db.deploys.latest(5);9 return rows.map((d) => (10 <Text key={d.id}>{`${d.state} ${d.branch}`}</Text>11 ));12}1314export default function Page() {15 return (16 <Box className="flex-col p-1">17 <Text bold>Deployments</Text>18 <Suspense fallback={<Text color="muted">Loading…</Text>}>19 <Deploys />20 </Suspense>21 </Box>22 );23}Also runs in terminals and agents
Code with OpenCode in DevTools
OpenCode Lite is the OpenCode TUI rebuilt with Text.js to run in your browser console.
Sandbox
A scripted agent answers any prompt, with nothing to install.
One command
Run bunx @textjs/opencode@latest proxy and enter the code it prints. The page connects to OpenCode on your machine.
Your models and sessions
Pick any model your server has configured with /models. Sessions, agents and models come from your OpenCode server.