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}Text.js supports concurrent rendering, Suspense and error boundaries.
Folders in app/ become routes and layouts, and Back, Forward and deep links work.
The grid fits the docked DevTools panel at any page zoom.
Keys, paste and IME composition reach the app through one hidden <textarea>.
console.log callA commit that changes no cells never reaches the console.
React 19
React commits through a custom reconciler.
Yoga lays out boxes on whole cells.
Only damaged rows repaint.
Row hash
9f1c08e2a07e5b3131b2c4d9e8d046fa0 calls · 0 B
Unchanged frames stop here.
3,200 cells become 149 %c spans.
Frames are appended at least 66ms apart.
Changed frame Unchanged frame, 0 calls
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
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}OpenCode Lite is the OpenCode TUI rebuilt with Text.js to run in your browser console.
A scripted agent answers any prompt, with nothing to install.
Run bunx @textjs/opencode@latest proxy and enter the code it prints. The page connects to OpenCode on your machine.
Pick any model your server has configured with /models. Sessions, agents and models come from your OpenCode server.