opentui

Build terminal UIs with OpenTUI. Covers Core, frameworks, components, application APIs, testing, extensions, integrations, deployment, and public API lookup.

Install
npx skills add 'https://github.com/anomalyco/opentui/tree/main/packages/web/src/content'
Download bundle ↓
main · ac753b4Scanned 2026-09-15

Contributors

GitHub-linked commit authors for this SKILL.md at the saved revision. Co-authors and history before file renames are not included.

File history ↗

docs/components/image.mdx

docs/components/image.mdxBrowse 89 files
View on GitHub
← Back to SKILL.md

title: Image description: Load and render images with Kitty, Sixel, or Unicode blocks skill: entry: true intents: [image, image-renderable, image-display, kitty, sixel]

Image

ImageRenderable displays PNG, JPEG, WebP, or GIF data from an encoded source. It also accepts an existing NativeImage. The renderable uses Kitty graphics, Sixel, or a Unicode block fallback.

Use NativeImage when you need to decode, inspect, or transform image pixels before display.

Availability

FieldAvailability
Package@opentui/core
Core renderableImageRenderable
React<image> (automatic)
Solid<image> (automatic)
StatusBuilt in

Renderable API

import { ImageRenderable, createCliRenderer } from "@opentui/core"

const renderer = await createCliRenderer()
const image = new ImageRenderable(renderer, {
  id: "cover",
  source: "./cover.webp",
  width: 40,
  height: 15,
  fit: "cover",
  protocol: "auto",
  onError: console.error,
})

renderer.root.add(image)
await image.loadPromise

source accepts every ImageSource form: a path, file:/HTTP(S)/blob:/data: URL, URL, Blob, Response, Uint8Array, or ArrayBuffer. It also accepts NativeImage. Format detection uses encoded bytes when the source needs decoding. ImageRenderable retains an existing NativeImage. You still own and must dispose the source reference.

React

import { createCliRenderer } from "@opentui/core"
import { createRoot } from "@opentui/react"

const renderer = await createCliRenderer()
createRoot(renderer).render(
  <image source="./cover.webp" fit="cover" protocol="auto" style={{ width: 40, height: 15 }} />,
)

Solid

import { createCliRenderer } from "@opentui/core"
import { render } from "@opentui/solid"

const renderer = await createCliRenderer()
await render(
  () => <image source="./cover.webp" fit="cover" protocol="auto" style={{ width: 40, height: 15 }} />,
  renderer,
)

You can update source, callbacks, fit, and protocol after construction. Replacing source keeps the current image visible until the replacement succeeds. It cancels obsolete loading and disposes stale native images. Setting source to undefined clears it. Clearing fit or protocol restores "fit" or "auto".

Sizing

fitBehavior
fitContain and center the full image. Preserve aspect (default)
coverFill the renderable, preserve aspect ratio, and center-crop
fillFill the renderable and allow stretching

Sizing uses terminal pixel resolution when available and a 2:1 cell-height fallback otherwise. During startup and resize, Sixel images temporarily use blocks until current pixel geometry arrives.

Rendering protocol

protocolBehavior
autoGlobal override, then Kitty, then Sixel, then Unicode blocks
kittyForce Kitty graphics
sixelForce Sixel. Falls back to blocks without terminal pixel resolution
blocksPortable Unicode quadrant-block rendering

The blocks protocol renders generated RGBA pixels as terminal cells:

████████████████████████
██████████████████▘██▜██
█████▛▀▝▀█████████▖██▟██
██▛▀██████▝▀█▛▀▘██▀▀████
▀███████████▗▄▙▄██████▀▀
████████▗▄▟███████▄▄████
▄▄▄▄▄▄▄▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▄▄
████████████████████████

With global and per-image protocols set to auto, tmux uses blocks. Explicit Kitty, or Sixel with pixel resolution, uses tmux passthrough.

Overlapping images must use the same effective protocol. OpenTUI does not support layering or alpha composition across different effective protocols. Leave overlapping images on auto or give them the same explicit protocol. Non-overlapping images can use different protocols.

Kitty preserves image alpha, Sixel treats alpha below 128 as transparent, and blocks blend sampled alpha. Placement opacity scales Kitty and block alpha. Sixel dims toward cell backgrounds. Direct, unbuffered fills, text, and box borders cover images at whole-cell granularity without blending.

Use OPENTUI_IMAGE_PROTOCOL=auto|kitty|sixel|blocks to set the global default. OPENTUI_GRAPHICS=false disables Kitty and Sixel detection. See Environment variables.

Split-footer scrollback snapshots use the same protocol resolution as live images. They use Kitty placement, Sixel with detected pixel geometry, or Unicode quadrant blocks. Snapshots with mixed effective protocols, overlapping images, or covered Sixel cells use blocks. Native scrollback images can render before the footer is pinned. Each image must fit within the output width and the height available above the pinned footer, and its snapshot must start at the beginning of a line. Await loadPromise before rendering an image into a ScrollbackSurface. See Writing to scrollback.

resolveImageRenderProtocol(requested, capabilities, hasResolution) exposes the same protocol-resolution policy for code that needs it without constructing a renderable.

Kitty transport

Set kittyImageTransport when you create the renderer, or assign renderer.kittyImageTransport to change it at runtime. It controls image transmission, not protocol selection or placement. Kitty commands still use the renderer's output backend.

const renderer = await createCliRenderer({ kittyImageTransport: "zlib" })
renderer.kittyImageTransport = "file"
// After a transmission, inspect the actual choice rather than assuming the request succeeded.
const status = renderer.kittyImageTransportStatus
  • "raw" is the default. It sends existing encoded PNG data unchanged. Otherwise, opaque pixels use RGB24, and other pixels use RGBA32.
  • "zlib" tries native level-1 compression. It uses raw if compression expands the stream, exceeds the 64 MiB preparation limit, or cannot allocate buffers. Encoded PNG data stays unchanged.
  • "file" writes a temporary file and sends its path for the local terminal to read. It still copies pixels, without shared memory or GPU sharing.

Changing transport retransmits visible Kitty images without replacing their sources. File negotiation remains asynchronous. Selecting an inline mode keeps outstanding files alive until their acknowledgments or deadlines, so queued output can still use them. Switching back to file reuses the session's probe result; it does not reset a failed or cancelled session.

Scrollback snapshots never use file transport because the renderer does not retain their sources for retries. They use self-contained raw or PNG output, or zlib when requested.

File transport

File transport requires a compatible local POSIX terminal. Windows, remote sessions, disabled graphics, and multiplexers use raw/PNG instead. Custom stdout defaults to remote. Set remote: false only if it connects to a local terminal.

File transport requires both a successful t=f query with a real file and an upload acknowledgment with an explicit image ID. Older WezTerm versions that answer queries but omit upload acknowledgments stay on raw/PNG. These checks are asynchronous, so initial frames can use raw/PNG while fileState is "probing".

Each file has a unique name, permissions 0600, and contents that do not change. The renderer uses the TMPDIR value forwarded at creation, or /tmp when absent. If you override forwardEnvKeys, include TMPDIR.

Each renderer allows at most eight pending files and 64 MiB of pending data. If an image ID is busy or either limit is reached, the renderer uses raw/PNG without replacing pending files.

A matching Kitty reply lets the renderer remove the file. Frame events, stdout drain, and cursor reports do not confirm that the terminal read or displayed an image.

The renderer checks each file's five-second deadline during rendering and once per second while idle. A timeout does not confirm that the terminal read the file.

Timeouts, terminal or output errors, cancelKittyImageTransport(), suspend, and destroy disable file uploads and attempt to remove pending files. Active images retry with raw/PNG after a failed upload. Late replies cannot re-enable file transport. Cleanup failures or termination that bypasses cleanup, such as SIGKILL, can leave files behind.

Transport status

kittyImageTransportStatus reports requested, effective, fileState, fallback, pendingFiles, and pendingBytes. The effective value describes the last transmission: raw, zlib, png, or file. The compression fallback means compression did not save space or was unavailable. File transport preserves PNG payloads, but still reports effective: "file".

Options and state

MemberTypeDescription
sourceImageRenderableSourceEncoded image source or NativeImage. Optional
fit"fit" | "cover" | "fill"Destination sizing
protocol"auto" | "kitty" | "sixel" | "blocks"Requested rendering protocol
onLoad(image: NativeImage) => voidCalled after the current source loads
onError(error: unknown) => voidCalled when the current source fails
imageNativeImage | nullCurrently displayed renderable-owned image
loadingbooleanWhether the current source is loading
loadErrorunknownCurrent load error, otherwise null
loadPromisePromise<void> | nullSettles after the current load attempt
effectiveProtocolkitty | sixel | blocksCurrent resolved protocol. Can change with capabilities or size
cellAspectRationumberPhysical or fallback cell aspect ratio
getFittedSize(...)(width, height, cellAspect?, sourceWidth?, sourceHeight?)Resolve destination cells. Omitted overrides use current values

The renderable owns image and the NativeImage passed to onLoad. Do not dispose or transfer them. Current-source failures set loadError, call onError, and resolve loadPromise. Superseded, cleared, or destroyed loads resolve without callbacks. Exceptions from either callback reject after state settles. Successful replacement, clearing, and destruction release owned images. A failed replacement keeps the current image.

Use FrameBuffer when you need direct cell drawing or OptimizedBuffer.drawImage(). Use QR code for text encoded as a scannable symbol.

Referenced from SKILL.md