ink-picture

June 2, 2026 · View on GitHub

Better image component for Ink CLI/TUI applications.

Display images in your terminal with automatic protocol detection and graceful fallbacks. Supports Sixel, Kitty image protocol, iTerm2 inline images, ASCII art, and more!

npm MIT License downloads

image

Who's using ink-picture?

Feel free to open a PR to showcase your project here!

Installation

npm install ink-picture

Quick Start

import React from "react";
import { Box, render } from "ink";
import Image, { InkPictureProvider } from "ink-picture";

function App() {
  return (
    <InkPictureProvider>
      <Image
        src="https://picsum.photos/200/200"
        width={20}
        height={10}
        alt="Example image"
      />
    </InkPictureProvider>
  );
}

render(<App />);

Important

Always wrap your app with InkPictureProvider.

Usage

<Image />

Drop-in image component with dynamic rendering protocol selection. This component can display images using multiple rendering methods depending on the terminal environment.

Props

src

type: string | ArrayBuffer | Buffer<ArrayBufferLike>

Source of the image. Accepts local file paths, URLs, and jimp raw image buffers. ink-picture uses jimp to process images internally, so it supports all image formats supported by jimp.

<Image src="path/to/image.png" />
<Image src="https://picsum.photos/200/200" />
<Image src="file:///path/to/image.jpg" />

width and height

type: number | string

default: "100%"

Dimensions of the image in terms of terminal cells. You can also set them as percentages to have them calculated based on the dimensions of the parent element. The image will be resized to fit the given dimensions.

When using percentage dimensions, the parent <Box> must have a determinable size, otherwise the image size cannot be calculated.

The height of a cell is usually around twice its width in most terminal emulators, so set the width and height to a 2:1 ratio for a roughly square image.

<Image
  src="path/to/image.png"
  width={10}
  height={5}
/>

// Image dimensions will be 12W X 8H
<Box width={20} height={10}>
  <Image
    src="path/to/image.png"
    width="60%"
    height="80%"
  />
</Box>

// Height will default to "100%"
<Image
  src="path/to/image.png"
  width={10}
/>

// This will not work because parent size is unbounded
<Box>
  <Image
    src="path/to/image.png"
    width="100%"
    height="100%"
  />
</Box>

alt (optional)

type: string

Alternative text displayed during image loading and error. It is also used in Ink's screen reader mode.

<Image src="path/to/image.png" alt="a close-up shot of a red brick house" />

protocol (optional)

type: ImageProtocolName | ImageProtocolHint

ImageProtocolName is one of "kitty" | "sixel" | "iterm2" | "halfBlock" | "braille" | "ascii". See Rendering protocols for details.

ImageProtocolHint is an object containing one or more key-value pairs of Visibility: ImageProtocolName.

If protocol is not set, the image will be rendered using a graphical rendering protocol when it is fully visible, if the terminal supports it. A text-based protocol will be used when the image is or becomes partially or entirely outside the terminal viewport.

Learn why

Graphical rendering protocols work by moving the cursor to the top-left of the image's location using ANSI escape sequences, and then writing the image data to stdout. However, the cursor cannot be positioned outside of the terminal's viewport (e.g. negative row or column indices); it will simply be clipped to the edge of the terminal. If we attempt to write an image that is outside of the terminal viewport, the image will be dislocated.

This is not a problem for text-based protocols because they follow Ink's normal rendering cycle. Overflow content will be clipped internally by Ink, and Ink clears the entire terminal before writing a new frame if the app is taller than the number of terminal rows.

Given this, the Image component dynamically switches to a text-based protocol for overflown images so at least something is safely shown.

If one of ImageProtocolName is passed, the corresponding rendering protocol will always be used where available, regardless of terminal capabilities. The image will not be rendered using a text-based protocol fallback when it is partially or entirely outside the terminal viewport; it simply doesn't render there.

If an ImageProtocolHint object is passed, the image will be rendered using the corresponding protocol for each specified Visibility state. Unspecified states will use the default protocol fallback behavior.

// Image will be rendered with sixels when fully visible,
// and skip rendering altogether otherwise.
<Image
  src="path/to/image.png"
  protocol="sixel"
/>

// Image will be rendered with kitty graphics protocol when fully visible,
// and fall back to a text-based protocol otherwise.
<Image
  src="path/to/image.png"
  protocol={{ "full": "kitty" }}
/>

See Visibility for details on visibility states and dynamic protocol switching.

getVisibility (optional)

type: (info: VisibilityInfo) => "full" | "partial" | "hidden"

A callback customizing the visibility detection logic of the image. It receives the absolute position and dimensions of the image in the terminal viewport, and should return a visibility state. This callback is useful for controlling dynamic protocol switching in apps with complex layouts (e.g. sticky headers and footers) since the image by itself does not know about whether it is obstructed by another element.

By default, images determine their visibility by checking against the dimensions of the app and the terminal viewport respectively.

See Visibility for more on how visibility detection works.

<InkPictureProvider />

Wrapper component that Detects and provides terminal capabilities, as well as configuration and image caching to all descendant Image components.

TerminalInfoProvider is exported as an alias for InkPictureProvider for backwards compatibility.

Upon mounting, the provider queries the terminal for capabilities using escape sequences. This temporarily intercepts stdin for up to 1 second (usually much shorter). Any user keystrokes made during this window are safely buffered and re-injected into your app once detection completes.

Tip

Move InkPictureProvider as close to the app's root as possible, i.e. wrap your entire app with it, so terminal capabilities are only detected once and provided to the entire app.

Props

terminalInfo (optional)

type: Partial<TerminalInfo>

Override detected terminal information and capabilities. Provide one or more of the following:

FieldDescription
terminalWidthTerminal viewport width in pixels
terminalHeightTerminal viewport height in pixels
cellWidthWidth of each terminal cell in pixels
cellHeightHeight of each terminal cell in pixels
supportsUnicodeWhether the terminal supports Unicode
supportsColorWhether the terminal supports colored output
supportsSixelGraphicsWhether the terminal supports Sixels
supportsKittyGraphicsWhether the terminal supports the Kitty Graphics Protocol
supportsITerm2GraphicsWhether the terminal supports iTerm2 Inline Images Protocol

config (optional)

type: Partial<InkPictureConfig>

Configurations for library-wide behavior.

FieldDefaultDescription
pollIntervalMs16Interval in milliseconds for polling for layout changes in the app
paintIntervalMs16Interval in milliseconds for redrawing graphical protocol images
cacheSize10Maximum number of cached images. Set it to 0 to disable caching

See Configuration for details.

onTerminalInfoDetection (optional)

type: (info: TerminalInfo) => void

Callback invoked after terminal capability detection completes. Use this if you want to conditionally run code based on terminal information.

<InkPictureProvider
  config={{ cacheSize: 20 }}
  onTerminalInfoDetection={(info) => {
    foo(info);
  }}
>
  <App />
</InkPictureProvider>

Individual components for each protocol

Each rendering protocol has a standalone component export. They all accept the same ImageProps (src, width, height, alt) and can be used directly when you want more low-level control:

import {
  AsciiImage,
  BrailleImage,
  HalfBlockImage,
  SixelImage,
  KittyImage,
  ITerm2Image,
} from "ink-picture";

All protocol components must still be wrapped in <InkPictureProvider>. They only handle visibility detection by checking against app and terminal viewport dimensions.

See Protocols for differences between each component.

Hooks

The following hooks are also exported for use in custom components:

  • useInkPictureConfig(): returns the resolved InkPictureConfig combining defaults and overrides
  • useTerminalInfo(): returns the resolved TerminalInfo combining defaults, detected terminal info, and overrides.
  • useImageCache(): returns the image cache instance (or null if caching is disabled)

If useTerminalInfo() is used outside of an InkPictureProvider, the following defaults will be used. Image rendering quality may degrade significantly with the default values; they are only meant to be a safe fallback.

FieldDefault
terminalWidth6 * process.stdout.columns
terminalHeight12 * process.stdout.rows
cellWidth6
cellHeight12
supportsUnicodefalse
supportsColorfalse
supportsSixelGraphicsfalse
supportsKittyGraphicsfalse
supportsITerm2Graphicsfalse

Rendering protocols

Images can be rendered using one of the following protocols:

IDNameResolutionRequirements
kittyKitty Graphics ProtocolFullSupported terminals
iterm2iTerm2 Inline Images ProtocolFullSupported terminals
sixelSixelFullSixel support (device attribute 4)
halfBlockColored unicode half-blocks (▄)1x2 per cellUnicode + color support
brailleMonochrome Braille patterns2x4 per cellUnicode support
asciiASCII art1x1 per cellNone (color support for colored output)

Compatibility with terminal emulators

ink-picture should work out-of-the-box in most terminal emulators and select the best supported protocol. Yet, terminal emulators may lack support for certain protocols and/or have non-compliant or faulty implementations for some protocols.

Use the table below as a reference to check which protocol to use for your terminal. You might also want to install a better terminal emulator for best experience.

✅ = Fully supported
⚠️ = Partially supported (works but may have issues or caveats)
❌ = Not supported

Terminal EmulatorSixelkitty graphicsiTerm2 inline images protocol
GNOME Terminal
Ghostty
iTerm21
Kitty
Konsole 2⚠️⚠️
Rio
xterm.js 3
(VS Code integrated terminal) 4
⚠️⚠️⚠️
Warp 5⚠️
WezTerm⚠️ 6
Windows Terminal
XTerm

Please refer to Are We Sixel Yet? for a comprehensive (but slightly out-of-date) list of terminals that support Sixel graphics.

If you know your terminal emulator supports any of the above protocols but is not listed here, please open an issue or PR to update the table.

Protocol selection

When no protocol prop is specified, Image selects the best protocol automatically:

  1. A graphical protocol (kitty > iTerm2 > Sixel, in that order) is chosen for fully visible images if the terminal supports it. The priority order is adjusted per-terminal to avoid known issues (see the heuristic in getBestProtocol).
  2. If no graphical protocol is supported, a text-based protocol is used instead: halfBlock when both Unicode and color are available, braille when only Unicode is available, or ascii as the universal fallback.
  3. When an image is partially or entirely outside the app or terminal viewport, it dynamically switches to a text-based protocol to avoid image dislocation.
Note on graphical protocols (sixel, kitty, iterm2) Images rendered with sixel and iterm2 protocols may experience flickers during app re-renders. This is because Ink clears the terminal buffer before rendering each frame, which removes any sixel and iterm2 images, even if their positions are unchanged. The images are repainted in short intervals (default 16 ms), so they reappear almost instantly, but may still produce a short flicker. The chance of flickering may be reduced with a shorter `paintIntervalMs` at the cost of potential performance degradation.

In addition, images rendered with sixel, kitty, and iterm2 protocols may not persist after app exit. This is because these renderers perform image cleanup upon unmount to prevent graphical artifacts, but they cannot distinguish between a regular React component unmount and an app exit. Issues and PRs addressing this bug are much appreciated.

Visibility

Each Image component tracks its absolute position in the terminal and determines whether it is fully visible, partially visible, or hidden relative to the terminal viewport and the app bounds. This drives the automatic protocol switching described above.

You can customize visibility detection with the getVisibility callback:

<Image
  src="image.png"
  getVisibility={({
    position,
    terminalWidth,
    terminalHeight,
    defaultVisibility,
  }) => {
    // Custom logic, e.g. account for a sticky header that overlaps the image
    if (position.row < 5) return "partial";
    return defaultVisibility;
  }}
/>

The callback receives a VisibilityInfo object:

FieldDescription
positionAbsolute position and dimensions (col, row, width, height, appWidth, appHeight)
terminalWidthTerminal viewport width in cells
terminalHeightTerminal viewport height in cells
defaultVisibilityThe visibility state computed by the built-in algorithm

The useVisibility hook and usePosition hook are also exported if you need visibility logic in custom components.

Screen reader accessibility

When Ink's screen reader mode is active, Image renders an empty Box with an aria-label describing the image. The label is either the alt prop or the image source string. Image data is not loaded or rendered, keeping output clean for screen readers.

Configuration

Configuration can be set through the config prop on <InkPictureProvider> or via environment variables. Environment variables take precedence over prop values.

PropEnv variableDefaultDescription
pollIntervalMsINK_PICTURE_POLL_INTERVAL16Interval in milliseconds for polling element layout changes.
paintIntervalMsINK_PICTURE_PAINT_INTERVAL16Interval in milliseconds for redrawing sixel and iterm2 protocol images.
cacheSizeINK_PICTURE_CACHE_SIZE10Number of decoded images to keep in memory. Set to 0 to disable.

Contributing

Contributions are welcome! To contribute:

  1. Open or comment on an issue describing what you want to change
  2. Fork the repository
  3. Create a feature branch: git checkout -b feature/amazing-feature
  4. Install dependencies: pnpm install
  5. Make your changes
  6. Run tests: pnpm test
  7. Open a pull request

License

MIT

Footnotes

  1. Needs verification.

  2. sixel and iip images cannot be erased by writing characters over them.

  3. sixel and iip images cannot be erased by writing characters over them due to a bug; kitty has non-compliant implementation.

  4. VS Code integrated terminal uses xterm.js. The settings terminal.integrated.enableImages and terminal.integrated.gpuAcceleration must be enabled to render graphical images.

  5. Information may be out-of-date. Kitty has non-compliant implementation.

  6. Kitty has non-compliant implementation.