---
title: "Move focus between panes"
audience: "anyone whose terminal tool has more than one pane"
description: "You'll split the task list into a list pane and a detail pane, move between them with Tab and Shift+Tab and give each pane its own keys."
snippet_caption: "app/routes/page.tsx"
snippet: "const { isFocused } = useFocus({ id: \"list\", autoFocus: true });\nuseInput(\n  (input, key) => {\n    if (key.upArrow) onMove(-1);\n  },\n  { isActive: isFocused && active },\n);"
---

import { Install } from "@src/ui/Install";

You'll split the task list into a list pane and a detail pane, move between them with Tab and Shift+Tab and give each pane its own keys. It starts from the app you built in [640](https://snapfirers.com/fsr/learn/640-type-into-the-app).

<Install captured="0.x" />

## Focus in Ink

`useFocus` makes a component focusable and tells it whether it has the focus. Tab moves the focus to the next focusable component in the order they render and Shift+Tab moves it back, wrapping at either end. `autoFocus: true` gives a component the focus when nothing else has it. An `id` lets you move the focus to it by name.

`useFocusManager` moves the focus from code: `focus(id)`, `focusNext`, `focusPrevious`, `enableFocus`, `disableFocus` and `activeId`.

Every `useInput` hears every key. Its second argument, `{ isActive }`, turns a handler off. A pane that should only answer while it has the focus passes `{ isActive: isFocused }`.

## Write the two panes

Add `useFocus` and `useFocusManager` to the `ink` import:

```tsx title="app/routes/page.tsx"
import { Box, Spacer, Text, useFocus, useFocusManager, useInput } from "ink";
```

Then add the panes above `Tasks`. The list pane takes the arrow keys and space. The detail pane shows the selected task and takes `x`, which removes it:

```tsx title="app/routes/page.tsx"
type ListProps = { tasks: Task[]; selected: number; active: boolean; onMove: (by: number) => void; onToggle: () => void };

function ListPane({ tasks, selected, active, onMove, onToggle }: ListProps) {
  const { isFocused } = useFocus({ id: "list", autoFocus: true });
  useInput(
    (input, key) => {
      if (key.upArrow) onMove(-1);
      if (key.downArrow) onMove(1);
      if (input === " ") onToggle();
    },
    { isActive: isFocused && active },
  );
  return (
    <Box flexDirection="column" flexGrow={1} flexBasis={0} borderStyle="round" borderColor={isFocused ? "cyan" : "gray"} paddingX={1}>
      {tasks.map((task, index) => (
        <Text key={task.title} bold={index === selected} dimColor={task.done} strikethrough={task.done}>
          {index === selected ? "> " : "  "}
          {task.title}
        </Text>
      ))}
    </Box>
  );
}

type DetailProps = { task: Task | undefined; active: boolean; onRemove: () => void };

function DetailPane({ task, active, onRemove }: DetailProps) {
  const { isFocused } = useFocus({ id: "detail" });
  useInput(
    (input) => {
      if (input === "x") onRemove();
    },
    { isActive: isFocused && active },
  );
  return (
    <Box flexDirection="column" flexGrow={1} flexBasis={0} borderStyle="round" borderColor={isFocused ? "cyan" : "gray"} paddingX={1}>
      {task ? (
        <>
          <Text bold>{task.title}</Text>
          <Text color={task.done ? "green" : "yellow"}>{task.done ? "done" : "open"}</Text>
          <Text dimColor>x removes it</Text>
        </>
      ) : (
        <Text dimColor>nothing selected</Text>
      )}
    </Box>
  );
}
```

Each pane runs in place with its own focus and its own handler. The border is cyan on the pane that has the focus and gray on the other. `active` is false while the text box from 640 is open, so typing a space or an `x` into it reaches neither pane. `flexGrow={1}` with `flexBasis={0}` on both panes splits the row between them in equal halves.

## Hand the panes the state

The tasks and the selection stay in `Tasks`. The panes get them as props, with functions that change them. Replace the component:

```tsx title="app/routes/page.tsx"
export default function Tasks({ name, where, tasks: initial, limit }: RootProps) {
  const [tasks, setTasks] = useState(initial);
  const [selected, setSelected] = useState(0);
  const [adding, setAdding] = useState(false);
  const [draft, setDraft] = useState("");
  const [saving, setSaving] = useState(false);
  const { focus } = useFocusManager();

  useEffect(() => {
    if (!saving) return;
    const timer = setTimeout(() => {
      save(where, tasks);
      setSaving(false);
    }, 500);
    return () => clearTimeout(timer);
  }, [tasks]);

  useInput((input, key) => {
    if (adding) {
      if (key.escape) setAdding(false);
      return;
    }
    if (input === "a") setAdding(true);
    if (input === "1") focus("list");
    if (input === "2") focus("detail");
  });

  const open = tasks.filter((task) => !task.done).length;
  const shown = tasks.slice(0, limit);

  const change = (next: Task[]) => {
    setTasks(next);
    setSaving(true);
  };

  const move = (by: number) => setSelected(Math.min(Math.max(selected + by, 0), shown.length - 1));
  const toggle = () => change(tasks.map((task, index) => (index === selected ? { ...task, done: !task.done } : task)));
  const remove = () => {
    change(tasks.filter((_, index) => index !== selected));
    setSelected(Math.max(selected - 1, 0));
  };
  const add = (title: string) => {
    setAdding(false);
    setDraft("");
    if (title.trim() !== "") change([...tasks, { title: title.trim(), done: false }]);
  };

  return (
    <Box flexDirection="column" width={64}>
      <Box paddingX={1}>
        <Text bold>Tasks</Text>
        <Text dimColor> {name}</Text>
        <Spacer />
        <Text color={open === 0 ? "green" : "yellow"}>{open} open</Text>
      </Box>
      <Box>
        <ListPane tasks={shown} selected={selected} active={!adding} onMove={move} onToggle={toggle} />
        <DetailPane task={shown[selected]} active={!adding} onRemove={remove} />
      </Box>
      {adding ? (
        <Box paddingX={1}>
          <Text color="cyan">new: </Text>
          <TextInput value={draft} onChange={setDraft} onSubmit={add} placeholder="what needs doing" />
        </Box>
      ) : null}
      <Box paddingX={1}>
        <Text dimColor>{adding ? "Enter adds, Esc cancels" : "Tab switches panes, a adds"}</Text>
        <Spacer />
        {saving ? (
          <Text color="yellow">
            <Spinner type="dots" /> saving
          </Text>
        ) : (
          <Text dimColor>saved</Text>
        )}
      </Box>
    </Box>
  );
}
```

The page's own handler has no `isActive`, so it hears every key whichever pane has the focus. `1` and `2` call `focus` with a pane's `id` to jump straight to it.

## Run it

Start from a list of three:

```shell-session
$ cat > ~/.tasks/tasks.json <<'EOF'
[{"title":"Write the README","done":true},{"title":"Tag a release","done":false},{"title":"Answer the open issues","done":false}]
EOF
$ fsr dev app --terminal
```

The list pane has the focus from the start:

```text
 Tasks tasks.json                                        2 open
╭──────────────────────────────╮╭──────────────────────────────╮
│ > Write the README           ││ Write the README             │
│   Tag a release              ││ done                         │
│   Answer the open issues     ││ x removes it                 │
╰──────────────────────────────╯╰──────────────────────────────╯
 Tab switches panes, a adds                               saved
```

Press Tab. The detail pane's border turns cyan. Press `x` and the README task is gone. Press Shift+Tab to go back to the list, then down and space:

```text
 Tasks tasks.json                                        1 open
╭──────────────────────────────╮╭──────────────────────────────╮
│   Tag a release              ││ Answer the open issues       │
│ > Answer the open issues     ││ done                         │
│                              ││ x removes it                 │
╰──────────────────────────────╯╰──────────────────────────────╯
 Tab switches panes, a adds                               saved
```

Space does nothing while the detail pane has the focus and `x` does nothing while the list has it, since each handler is only active in its own pane.

Next up: [660. Run git and show its output](https://snapfirers.com/fsr/learn/660-run-git-and-show-its-output).
