# 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.

```jsx title="app.jsx"

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`.

<Callout>
Run it locally with `voxal dev` to get a live preview that reloads on save.
</Callout>

## 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`.

```jsx title="app.jsx"

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.

```jsx title="app.jsx"

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.

```jsx title="app.jsx"

function Dashboard() {
  const { exit } = useApp();
  useInput((input, key) => {
    if (input === 'q') exit();
  });
  // ...
}
```

<Callout>
Ctrl-C exits by default and is not delivered to `useInput`, so you do not need to handle it yourself.
</Callout>

## 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.

```jsx title="app.jsx"

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

<Cards>
  <Card title="UI reference" href="/sdk/ui" description="Every component, hook, and style prop" />
  <Card title="Handle keyboard input" href="/guides/keyboard-input" description="The full key object and when to use core vs UI" />
  <Card title="Share state across connections" href="/guides/sharing-state" description="Per-connection vs shared state" />
</Cards>

---

Source: https://docs.voxal.sh/guides/building-uis
