Build a UI with React
Build a flexbox terminal UI with the voxal UI layer, from serve() to a live dashboard
The voxal UI layer (@voxalsh/sdk/ui) lets you build terminal interfaces with React and flexbox, exactly the way you would write them for the desktop with Ink. In this guide you will build a small dashboard UI: a bordered layout, styled text, keyboard handling, and a clean exit.
Mount a tree with serve()
Where the core SDK gives you createApp() and raw conn.write(), the UI layer gives you serve(). It mounts a React tree for every connection and wires input, resize, and close for you. There is no .listen() and no manual ANSI.
import React from 'react';
import { serve, Box, Text } from '@voxalsh/sdk/ui';
serve((conn) => (
<Box>
<Text color="cyan" bold>hello from voxal</Text>
</Box>
));The function you pass to serve() runs once per connection and returns the root element. The conn argument is the same connection object the core SDK exposes, so you can read conn.id, conn.cols, and conn.rows.
Run it locally with voxal dev to get a live preview that reloads on save.
Lay out with Box and Text
Box is a flexbox container. Text holds the characters that actually render. Anything visible lives inside a Text. Box accepts the layout props you would expect from flexbox:
flexDirection("row"or"column")padding,paddingX,paddingY,margin,gapborderStyle(for example"round"),borderColorwidth,justifyContent,alignItems
Text accepts color, backgroundColor, bold, dimColor, and inverse.
import React from 'react';
import { serve, Box, Text } from '@voxalsh/sdk/ui';
function Panel({ label, value }) {
return (
<Box flexDirection="column" borderStyle="round" borderColor="gray" paddingX={1}>
<Text dimColor>{label}</Text>
<Text color="green" bold>{value}</Text>
</Box>
);
}
serve(() => (
<Box flexDirection="row" gap={2} padding={1}>
<Panel label="status" value="online" />
<Panel label="region" value="lon" />
</Box>
));Read terminal size
Use useWindowSize() to react to the terminal dimensions. It returns { columns, rows } and re-renders when the user resizes their window, so your layout stays correct without any manual resize handling.
import { useWindowSize } from '@voxalsh/sdk/ui';
function Header() {
const { columns } = useWindowSize();
return <Text dimColor>{'─'.repeat(columns)}</Text>;
}Handle input and exit
useInput((input, key) => ...) delivers keystrokes. useApp() returns { exit }, which unmounts the tree and closes that connection. Press q to quit in the example below.
import { useInput, useApp } from '@voxalsh/sdk/ui';
function Dashboard() {
const { exit } = useApp();
useInput((input, key) => {
if (input === 'q') exit();
});
// ...
}Ctrl-C exits by default and is not delivered to useInput, so you do not need to handle it yourself.
The full dashboard
Putting it together: a header rule sized to the terminal, two stat panels, a counter you can change with the arrow keys, and q to quit.
import React, { useState } from 'react';
import {
serve,
Box,
Text,
Spacer,
useInput,
useApp,
useWindowSize,
} from '@voxalsh/sdk/ui';
function Panel({ label, value, color }) {
return (
<Box flexDirection="column" borderStyle="round" borderColor="gray" paddingX={1}>
<Text dimColor>{label}</Text>
<Text color={color} bold>{value}</Text>
</Box>
);
}
function Dashboard() {
const { columns } = useWindowSize();
const [count, setCount] = useState(0);
const { exit } = useApp();
useInput((input, key) => {
if (input === 'q') return exit();
if (key.upArrow) setCount((c) => c + 1);
if (key.downArrow) setCount((c) => c - 1);
});
return (
<Box flexDirection="column">
<Text color="cyan" bold>voxal dashboard</Text>
<Text dimColor>{'─'.repeat(Math.max(1, columns))}</Text>
<Box flexDirection="row" gap={2} paddingY={1}>
<Panel label="status" value="online" color="green" />
<Panel label="counter" value={String(count)} color="yellow" />
</Box>
<Box>
<Text dimColor>↑/↓ change counter</Text>
<Spacer />
<Text dimColor>q quit</Text>
</Box>
</Box>
);
}
serve((conn) => <Dashboard />);Deploy it with voxal deploy, then open it with ssh <name>@voxal.sh.