voxal docs
Guides

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.

app.jsx
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, gap
  • borderStyle (for example "round"), borderColor
  • width, justifyContent, alignItems

Text accepts color, backgroundColor, bold, dimColor, and inverse.

app.jsx
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.

app.jsx
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.

app.jsx
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.

app.jsx
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.

Next steps

On this page