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";
function App() {
return (
<Box flexDirection="column" padding={1}>
<Text color="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
<Box flexDirection="row" gap={2}>
<Text>Left</Text>
<Text>Right</Text>
</Box>
// Grid layout
<Box display="grid" gridTemplateColumns="1fr 2fr 1fr" gap={1}>
<Text>Col 1</Text>
<Text>Col 2</Text>
<Text>Col 3</Text>
</Box>
// With borders and padding
<Box borderStyle="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";
<Text color="blue">Blue text</Text>
<Text backgroundColor="red" color="white">Highlighted</Text>
<Text bold italic underline>Styled text</Text>
<Text color="#ff6600">Hex colors work too!</Text>The <Static> component renders static content that won't be updated. Perfect for logs and history.
import { Static, Text } from "tinky";
const logs = ["Log 1", "Log 2", "Log 3"];
<Static items={logs}>{(log, index) => <Text key={index}>{log}</Text>}</Static>;The <Transform> component allows you to transform the output of its children.
import { Transform, Text } from "tinky";
<Transform transform={(output) => output.toUpperCase()}>
<Text>hello</Text>
</Transform>;
// Renders: HELLOimport { Box, Text, Newline, Spacer } from "tinky";
// Newline - adds vertical space
<Box flexDirection="column">
<Text>Line 1</Text>
<Newline count={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";
function MyComponent() {
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";
function MyComponent() {
const { exit } = useApp();
// Exit with error
exit(new Error("Something went wrong"));
// Exit normally
exit();
}Manage focus for interactive components.
import { useFocus, Box, Text } from "tinky";
function FocusableItem({ label }: { label: string }) {
const { isFocused } = useFocus();
return (
<Box borderStyle={isFocused ? "bold" : "single"}>
<Text color={isFocused ? "green" : "white"}>{label}</Text>
</Box>
);
}Direct access to stdin, stdout, and stderr streams.
import { useStdout, useEffect } from "tinky";
function MyComponent() {
const { write } = useStdout();
useEffect(() => {
write("Hello from stdout!\n");
}, []);
return null;
}<Box
flexDirection="row" // row, row-reverse, column, column-reverse
justifyContent="center" // flex-start, flex-end, center, space-between, space-around
alignItems="center" // flex-start, flex-end, center, stretch
flexWrap="wrap" // nowrap, wrap, wrap-reverse
flexGrow={1}
flexShrink={0}
gap={2}
/><Box
display="grid"
gridTemplateColumns="1fr 2fr 1fr"
gridTemplateRows="auto 1fr"
columnGap={1}
rowGap={1}
justifyItems="center"
alignItems="center"
/><Box borderStyle="single" /> // โโโ
<Box borderStyle="double" /> // โโโ
<Box borderStyle="round" /> // โญโโฎ
<Box borderStyle="bold" /> // โโโ
<Box borderStyle="classic" /> // +--+Tinky supports multiple color formats:
<Text color="red" /> // Named colors
<Text color="#ff6600" /> // Hex colors
<Text color="rgb(255, 102, 0)" /> // RGB colors
<Text color="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 exit
await waitUntilExit();
// Rerender with new props
rerender(<App newProp={true} />);
// Unmount the app
unmount();
// Clear the output
clear();Measure the dimensions of a rendered element.
import { measureElement, Box, useRef, useEffect } from "tinky";
function MyComponent() {
const ref = useRef(null);
useEffect(() => {
if (ref.current) {
const { width, height } = measureElement(ref.current);
console.log(`Size: ${width}x${height}`);
}
}, []);
return <Box ref={ref}>Content</Box>;
}Tinky uses Bun for testing. Run the test suite:
bun testMIT ยฉ ByteLandTechnology