# Handle keyboard input

> Read keystrokes in both the core SDK and the UI layer, and know when to use each

Every voxal app is driven by keystrokes arriving over SSH. This guide covers both ways to read them: raw bytes in the core SDK (`onKey`), and the parsed `useInput` hook in the UI layer. By the end you will know which to reach for and how to match common keys.

## Core SDK: onKey with raw data

In the core SDK, `onKey((conn, data) => ...)` fires for each keypress. `data` is the raw string the terminal sends. Single characters arrive as themselves, and special keys arrive as control characters or ANSI escape sequences:

| Key | `data` value |
| --- | --- |
| Letter `q` | `"q"` |
| Enter | `"\r"` |
| Ctrl-C | `"\x03"` |
| Ctrl-D | `"\x04"` |
| Up arrow | `"\x1b[A"` |
| Down arrow | `"\x1b[B"` |
| Right arrow | `"\x1b[C"` |
| Left arrow | `"\x1b[D"` |

Because you see the bytes directly, you match them with plain string comparisons.

```js title="app.js"

createApp()
  .onConnect((conn) => {
    conn.write('press q to quit, arrows to move\r\n');
  })
  .onKey((conn, data) => {
    if (data === 'q' || data === '\x03' || data === '\x04') return conn.close();
    if (data === '\r') return; // ignore bare Enter
    if (data === '\x1b[A') return conn.write('up\r\n');
    if (data === '\x1b[B') return conn.write('down\r\n');
    conn.write('you pressed ' + JSON.stringify(data) + '\r\n');
  })
  .listen();
```

<Callout type="warn">
The core SDK does not send a newline for you. Write `"\r\n"` to end a line, and remember that Ctrl-C arrives as `"\x03"` so you decide what it does (close the connection is a sensible default).
</Callout>

## UI layer: useInput and the key object

In the UI layer, `useInput((input, key) => ...)` parses keystrokes for you. `input` is the typed character (empty for non-printing keys), and `key` is a structured object of booleans:

- Arrows: `key.upArrow`, `key.downArrow`, `key.leftArrow`, `key.rightArrow`
- Editing: `key.return`, `key.backspace`, `key.delete`, `key.tab`, `key.escape`
- Modifiers: `key.ctrl`, `key.shift`, `key.meta`

You never parse escape sequences yourself.

```jsx title="app.jsx"

function Editor() {
  const [text, setText] = useState('');
  const { exit } = useApp();

  useInput((input, key) => {
    if (key.escape) return exit();
    if (key.backspace || key.delete) return setText((t) => t.slice(0, -1));
    if (input && !key.ctrl && !key.meta) setText((t) => t + input);
  });

  return (
    <Box borderStyle="round" paddingX={1}>
      <Text>{text}</Text>
      <Text inverse> </Text>
    </Box>
  );
}

serve(() => <Editor />);
```

The `input && !key.ctrl && !key.meta` check is the standard way to capture only real typed characters and skip modifier combinations.

<Callout>
Ctrl-C exits the app by default in the UI layer and is not delivered to `useInput`. To run a callback only while a component is focused, pass `{ isActive }`: `useInput(handler, { isActive: focused })`.
</Callout>

## Which one to use

<Cards>
  <Card title="Core SDK (onKey)" description="You want full control of the raw byte stream, a tiny app with no React, or custom escape sequence handling." />
  <Card title="UI layer (useInput)" description="You are building a React TUI. The parsed key object and per-component focus are almost always what you want." />
</Cards>

As a rule, reach for the UI layer unless you have a specific reason to handle raw bytes. Both run on the same host contract, so neither is faster than the other; the difference is ergonomics.

## Next steps

<Cards>
  <Card title="Build a UI with React" href="/guides/building-uis" description="Layout, components, and a live dashboard" />
  <Card title="Connection reference" href="/sdk/connection" description="conn.write, conn.id, cols and rows" />
  <Card title="useInput reference" href="/sdk/ui/hooks" description="The full hook and key object" />
</Cards>

---

Source: https://docs.voxal.sh/guides/keyboard-input
