The Console Framework

Build full-stack React apps that render inside console.log.

Get started

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
███████ █████ ████ █ █ █████ ██ ███
█████ █ █ █ █ █ █ █
███ █ ███ █ █ █ ██
█ █ █ █ █ █ █ █ █
█ ████ █ █ █ █ ██ ███
React 19 in console.log
▄▀█▄
█▀ █
▀▀
Geist Dark
↑ ↓ count t theme
viewport:3

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

Live
preview://table:1
↑ ↓ move · s sort · Space select
Docs
Layout
  • <Box>
  • <Stack>
  • <Grid>
  • <SplitPane>
  • <ScrollBox>
  • <Center>
Data
  • <List>
  • <Table>
  • <Tree>
  • <Markdown>
  • <Code>
  • <Diff>
Charts
  • <LineChart>
  • <BarChart>
  • <Sparkline>
  • <ProgressBar>
  • <Gradient>
  • <Image>
Controls
  • <Input>
  • <Select>
  • <Checkbox>
  • <Switch>
  • <Slider>
  • <Dialog>
Keyboard
  • useKeymap
  • <KeyHelp>
  • <KeyHints>
  • <CommandPalette>
  • <StatusBar>
  • useFocus

Every changed frame is one console.log call

A commit that changes no cells never reaches the console.

  1. React 19

    01Commit

    React commits through a custom reconciler.

  2. Yoga

    02Layout

    Yoga lays out boxes on whole cells.

  3. CellBuffer

    03Paint

    Only damaged rows repaint.

  4. Row hash

    04Dirty gate

    Unchanged frames stop here.

  5. RLE

    05Encode

    3,200 cells become 149 %c spans.

  6. console.log

    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 post
clear() + 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
100×32 dashboard, mean of 1,000 frames · Apple M5 Pro · Bun 1.4.4Turbolog engine docs

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}

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.