---
title: "Lay out a task list"
audience: "anyone who has run the Ink counter from 600"
description: "You'll turn the counter into a task list with a header, a selected row you move with the arrow keys and a footer of keys, all laid out with Ink's flexbox props."
snippet_caption: "app/routes/page.tsx"
snippet: "<Box>\n  <Text bold>Tasks</Text>\n  <Spacer />\n  <Text color={open === 0 ? \"green\" : \"yellow\"}>{open} open</Text>\n</Box>"
---

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

You'll turn the counter into a task list with a header, a selected row you move with the arrow keys and a footer of keys, all laid out with Ink's flexbox props. It starts from the app you built in [600](https://snapfirers.com/fsr/learn/600-run-an-ink-counter-in-the-terminal).

<Install captured="0.x" />

## Boxes are flexbox

A `Box` lays out its children the way a CSS flex container does. `flexDirection="column"` stacks them and the default `row` puts them side by side. `width`, `padding`, `margin`, `gap`, `justifyContent` and `alignItems` mean what they mean in CSS, counted in terminal cells. `borderStyle` draws a border: `single`, `double`, `round`, `bold` and `classic` among others, colored with `borderColor`.

Two more names help with layout. `Spacer` takes up the free space in its row, so whatever follows it is pushed to the far edge. `Newline` breaks the line inside a `Text`.

## Write the list

Replace the counter with this page:

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

type Task = { title: string; done: boolean };

const START: Task[] = [
  { title: "Write the README", done: true },
  { title: "Tag a release", done: false },
  { title: "Answer the open issues", done: false },
];

export default function Tasks() {
  const [tasks, setTasks] = useState(START);
  const [selected, setSelected] = useState(0);

  useInput((input, key) => {
    if (key.upArrow && selected > 0) setSelected(selected - 1);
    if (key.downArrow && selected < tasks.length - 1) setSelected(selected + 1);
    if (input === " ") {
      setTasks(tasks.map((task, index) => (index === selected ? { ...task, done: !task.done } : task)));
    }
  });

  const open = tasks.filter((task) => !task.done).length;

  return (
    <Box flexDirection="column" borderStyle="round" borderColor="cyan" paddingX={1} width={44}>
      <Box>
        <Text bold>Tasks</Text>
        <Spacer />
        <Text color={open === 0 ? "green" : "yellow"}>{open} open</Text>
      </Box>
      <Box flexDirection="column" marginY={1}>
        {tasks.map((task, index) => (
          <Box key={task.title}>
            <Text color="cyan">{index === selected ? "> " : "  "}</Text>
            <Text bold={index === selected} dimColor={task.done} strikethrough={task.done}>
              {task.title}
            </Text>
          </Box>
        ))}
      </Box>
      <Text dimColor>
        up and down move, space marks one done
        <Newline />
        Ctrl-C quits
      </Text>
    </Box>
  );
}
```

The header is a row: the title, a `Spacer`, then the count, which lands against the right border. The rows are a column with a margin above and below. The footer is one `Text` broken in two by a `Newline`.

`Text` takes the styles: `color`, `backgroundColor`, `bold`, `italic`, `underline`, `strikethrough`, `inverse` and `dimColor`. A color is a name such as `cyan`, a hex value such as `#ff8800` or `rgb(255, 136, 0)`.

## Read the arrow keys

`useInput` hands the handler a second argument, `key`. For a named key `input` is empty and `key` says which one it was: `upArrow`, `downArrow`, `leftArrow`, `rightArrow`, `pageUp`, `pageDown`, `return`, `escape`, `tab`, `backspace` and `delete`, plus `ctrl`, `shift` and `meta` for the modifiers. Space is a character, so it arrives as `input === " "`.

The handler only sets state. The list and the count are worked out again from that state on the next render.

## Run it

```shell-session
$ fsr dev app --terminal
```

```text
╭──────────────────────────────────────────╮
│ Tasks                             2 open │
│                                          │
│ > Write the README                       │
│   Tag a release                          │
│   Answer the open issues                 │
│                                          │
│ up and down move, space marks one done   │
│ Ctrl-C quits                             │
╰──────────────────────────────────────────╯
```

The first task is done, so it is dimmed and struck through. Press down, space, down, down and space. The last down finds the selection already on the last row and does nothing, because the handler stops there:

```text
╭──────────────────────────────────────────╮
│ Tasks                             0 open │
│                                          │
│   Write the README                       │
│   Tag a release                          │
│ > Answer the open issues                 │
│                                          │
│ up and down move, space marks one done   │
│ Ctrl-C quits                             │
╰──────────────────────────────────────────╯
```

The count turns green at 0.

Next up: [620. Read the command line](https://snapfirers.com/fsr/learn/620-read-the-command-line).
