React for CLIs, re-imagined with the Taffy layout engine
Tinky is a modern React-based framework for building beautiful and interactive command-line interfaces. It leverages the powerful Taffy layout engine to provide CSS Flexbox and Grid layout support in the terminal.
- 🎨 React Components — Build CLIs using familiar React patterns and JSX syntax
- 📐 Flexbox & Grid Layout — Full CSS Flexbox and CSS Grid support powered by Taffy
- ⌨️ Keyboard Input — Built-in hooks for handling keyboard input and focus management
- 🎯 Focus Management — Tab/Shift+Tab navigation with customizable focus behavior
- 🖼️ Borders & Backgrounds — Rich styling with borders, background colors, and more
- ♿ Accessibility — Screen reader support with ARIA attributes
- 🔄 Hot Reloading — Fast development with React DevTools support
- 📦 TypeScript First — Full TypeScript support with comprehensive type definitions
# Using npm
npm install tinky react
# Using yarn
yarn add tinky react
# Using pnpm
pnpm add tinky react
# Using bun
bun add tinky reactimport{render,Box,Text}from"tinky";functionApp(){return(<BoxflexDirection="column"padding={1}><Textcolor="green"bold>
Hello, Tinky! 🎉
</Text><Text>Build beautiful CLIs with React</Text></Box>);}render(<App/>);The <Box> component is a fundamental building block. It's like a <div> in the browser, supporting Flexbox and Grid layouts.
import{Box,Text}from"tinky";// Flexbox layout<BoxflexDirection="row"gap={2}><Text>Left</Text><Text>Right</Text></Box>// Grid layout<Boxdisplay="grid"gridTemplateColumns="1fr 2fr 1fr"gap={1}><Text>Col 1</Text><Text>Col2</Text><Text>Col 3</Text></Box>// With borders and padding<BoxborderStyle="round"borderColor="cyan"padding={1}><Text>Styled Box</Text></Box>The <Text> component renders styled text with colors, bold, italic, and more.
import{Text}from"tinky";<Textcolor="blue">Blue text</Text><TextbackgroundColor="red"color="white">Highlighted</Text><Textbolditalicunderline>Styled text</Text><Textcolor="#ff6600">Hexcolorsworktoo!</Text>The <Static> component renders static content that won't be updated. Perfect for logs and history.
import{Static,Text}from"tinky";constlogs=["Log 1","Log 2","Log 3"];<Staticitems={logs}>{(log,index)=><Textkey={index}>{log}</Text>}</Static>;The <Transform> component allows you to transform the output of its children.
import{Transform,Text}from"tinky";<Transformtransform={(output)=>output.toUpperCase()}><Text>hello</Text></Transform>;// Renders: HELLOimport{Box,Text,Newline,Spacer}from"tinky";// Newline - adds vertical space<BoxflexDirection="column"><Text>Line 1</Text><Newlinecount={2}/><Text>Line 2</Text></Box>// Spacer - flexible space in flex containers<Box><Text>Left</Text><Spacer/><Text>Right</Text></Box>Handle keyboard input in your components.
import{useInput,useApp}from"tinky";functionMyComponent(){const{ exit }=useApp();useInput((input,key)=>{if(key.escape){exit();}if(key.upArrow){// Handle up arrow}if(input==="q"){exit();}});return<Text>Press 'q' to quit</Text>;}Access the app instance to control exit behavior.
import{useApp}from"tinky";functionMyComponent(){const{ exit }=useApp();// Exit with errorexit(newError("Something went wrong"));// Exit normallyexit();}Manage focus for interactive components.
import{useFocus,Box,Text}from"tinky";functionFocusableItem({ label }: {label: string}){const{ isFocused }=useFocus();return(<BoxborderStyle={isFocused ? "bold" : "single"}><Textcolor={isFocused ? "green" : "white"}>{label}</Text></Box>);}Direct access to stdin, stdout, and stderr streams.
import{useStdout,useEffect}from"tinky";functionMyComponent(){const{ write }=useStdout();useEffect(()=>{write("Hello from stdout!\n");},[]);returnnull;}<BoxflexDirection="row"// row, row-reverse, column, column-reversejustifyContent="center"// flex-start, flex-end, center, space-between, space-aroundalignItems="center"// flex-start, flex-end, center, stretchflexWrap="wrap"// nowrap, wrap, wrap-reverseflexGrow={1}flexShrink={0}gap={2}/><Boxdisplay="grid"gridTemplateColumns="1fr 2fr 1fr"gridTemplateRows="auto 1fr"columnGap={1}rowGap={1}justifyItems="center"alignItems="center"/><BoxborderStyle="single"/>// ┌─┐<BoxborderStyle="double"/>// ╔═╗<BoxborderStyle="round"/>// ╭─╮<BoxborderStyle="bold"/>// ┏━┓<BoxborderStyle="classic"/>// +--+Tinky supports multiple color formats:
<Textcolor="red"/>// Named colors<Textcolor="#ff6600"/>// Hex colors<Textcolor="rgb(255, 102, 0)"/>// RGB colors<Textcolor="ansi256:208"/>// ANSI 256 colorsFor complete API documentation, see the API Docs.
Render a React element to the terminal.
import{render}from"tinky";const{ unmount, waitUntilExit, rerender, clear }=render(<App/>,{stdout: process.stdout,stdin: process.stdin,stderr: process.stderr,exitOnCtrlC: true,patchConsole: true,});// Wait for the app to exitawaitwaitUntilExit();// Rerender with new propsrerender(<AppnewProp={true}/>);// Unmount the appunmount();// Clear the outputclear();Use incrementalRendering to control how Tinky updates interactive frames.
Run mode diffs terminal cells and writes minimal changed runs. Line mode diffs
whole lines and rewrites changed lines.
import{render}from"tinky";render(<App/>,{// Equivalent to: { strategy: "run" }incrementalRendering: true,});render(<App/>,{incrementalRendering: {strategy: "line"},});render(<App/>,{incrementalRendering: {enabled: false},});Tinky automatically falls back to non-run paths in debug, screen-reader, and
CI environments. For strategy trade-offs and behavior details, read
Incremental rendering guide.
Measure the dimensions of a rendered element.
import{measureElement,Box,useRef,useEffect}from"tinky";functionMyComponent(){constref=useRef(null);useEffect(()=>{if(ref.current){const{ width, height }=measureElement(ref.current);console.log(`Size: ${width}x${height}`);}},[]);return<Boxref={ref}>Content</Box>;}Tinky uses Bun for testing. Run the test suite:
bun testTo benchmark incremental rendering locally, run:
bun run perf:renderTo enforce the performance threshold used by CI, run:
bun run perf:gateMIT © ByteLandTechnology