Skip to content
Ink
Esc
↑↓navigate↵open⌘Jpreview
On this page

Screens and layout

Arrange content, headers and tabs.

Example

import { navigate, Screen, Text } from "ink";
import { moreHoriz } from "ink/icons";

export default function Home() {
  return <Screen
    title="Home"
    rightAction={{
      icon: moreHoriz,
      onPress: () => navigate("/actions"),
    }}
  >
    <Text>Welcome to Ink!</Text>
  </Screen>;
}

Create app/actions.tsx for the header button’s destination. Screen provides padding, navigation and scrolling.

Home screen with a right header action.

Header actions

Use leftAction and rightAction for header buttons. Back takes precedence over leftAction on detail screens.

import { Screen, navigate } from "ink";
import { graphicEq, sort } from "ink/icons";

<Screen
  title="Albums"
  leftAction={{ icon: sort, onPress: () => navigate("/albums-sort") }}
  rightAction={{ icon: graphicEq, onPress: () => navigate("/playing") }}
>
  {/* Screen content */}
</Screen>

Centred content

<Screen title="Home" centered>
  <Text>Welcome to Ink!</Text>
</Screen>
Welcome to Ink! centred on the page.

Stacks

Use Stack to group content. Set axis=“horizontal” for a row.

Set onPress to make the whole group tappable, including gaps between its children. Use haptic= to disable feedback for that action.

<Stack gap={8}>
  <Text>London</Text>
  <Text size={18}>United Kingdom</Text>
</Stack>
<Stack axis="horizontal" justify="space-between">
  <Text>Low</Text>
  <Text>High</Text>
</Stack>
A location grouped vertically above two horizontally spaced labels.

Space between

A vertical Stack with justify=“space-between” fills the content area when it is the only child of Screen.

import { Screen, Stack, Text } from "ink";

<Screen title="About">
  <Stack align="stretch" justify="space-between">
    <Text>My app</Text>
    <Stack align="center">
      <Text size={18}>Powered by</Text>
      <Text>Ink Framework</Text>
    </Stack>
  </Stack>
</Screen>
My app at the top, with Powered by above Ink Framework centred at the bottom.

Pinned content

header keeps content above a scrolling list.

<Screen title="Items" header={<Button onPress={addItem}>Add item</Button>}>
  <List items={items} keyExtractor={item => item.id}
    renderItem={item => <Text>{item.name}</Text>} />
</Screen>
An Add item button above a list.
The Add item button remains visible after scrolling.

Tabs

Declare tabs in _layout.tsx. Each name refers to a neighbouring page.

import { Tabs } from "ink";
import { homeFilled, settingsFilled } from "ink/icons";

export default function Layout() {
  return <Tabs>
    <Tabs.Screen name="index" icon={homeFilled} />
    <Tabs.Screen name="settings" icon={settingsFilled} />
  </Tabs>;
}

See Navigation for tab actions, page structure and history.

Home and settings icons in the bottom navigation bar.

Spacing and appearance

Screens and lists share a default gap of 47 logical units. Stack accepts gap, align and justify. Use gap={0} for compact groups; do not add padding around content already padded by the screen.

Background

Set background to a #RRGGBB colour to fill the screen, including its header. Text keeps the current colour scheme. The background follows colour-scheme inversion, like Canvas colours.

<Screen title="Railcard" background="#D35600">
  <Text>Card details</Text>
</Screen>

Bottom spacing

Set bottomInset={false} to let custom controls reach the bottom edge. The default leaves 20 logical units below the content.

<Screen title="Controls" bottomInset={false}>
  <Stack justify="space-between" gap={0}>
    <Text>Page content</Text>
    <Canvas width={350} height={60}>
      <Rectangle x={0} y={0} width={350} height={60} fill="#FFFFFF" />
      <CanvasText x={0} y={0} width={350} height={60} colour="#000000">Controls</CanvasText>
    </Canvas>
  </Stack>
</Screen>
Controls with the default space below them.
The same controls reaching the bottom edge of the screen.

Colour scheme

Use a toggle to switch between light and dark mode.

import { Screen, Toggle, setColourScheme, useColourScheme } from "ink";

export default function Appearance() {
  const scheme = useColourScheme();

  return <Screen title="Appearance">
    <Toggle label="Light mode" value={scheme === "light"}
      onChange={light => setColourScheme(light ? "light" : "dark")} />
  </Screen>;
}

The choice lasts for the app session. Use Store to save it between sessions. Photos and codes retain their original colours.

Wide content

Use wide to reduce horizontal margins to 20 units. Ink reserves space for the scrollbar when needed.

<Screen title="Albums" wide>
  <List items={albums} keyExtractor={album => album.id} renderItem={renderAlbum} />
</Screen>
Album list with normal content margins.
The same album list with wider content and smaller margins.

Loading images

Screens wait up to one second for their first visible images before appearing. Failed images do not block the screen. Images reached by scrolling load normally.

To show content immediately:

<Screen title="Show" waitForImages={false}>
  <Image src={show.artwork} width={100} height={100} fit="cover" />
  <Text>{show.title}</Text>
</Screen>