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/textarea.mdx

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

title: Textarea description: Edit multiple lines with cursor movement, selection, and key bindings

Textarea

Textarea edits multiple lines with cursor movement, selection, and configurable key bindings. Use Input for a single line.

Availability

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

Basic usage

Renderable API

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

const renderer = await createCliRenderer()

const textarea = new TextareaRenderable(renderer, {
  id: "notes",
  width: 50,
  height: 6,
  placeholder: "Type notes here...",
  backgroundColor: "#1a1a1a",
  focusedBackgroundColor: "#222222",
  textColor: "#FFFFFF",
  cursorColor: "#00FF88",
})

renderer.root.add(textarea)
textarea.focus()

At 30 columns, longer text wraps at word boundaries:

textarea.width = 30
textarea.setText("Long lines wrap at word boundaries.\nKeep paragraphs readable.")
Notes
Long lines wrap at word
boundaries.
Keep paragraphs readable.

Submit handling

Bind a submit action and listen for onSubmit:

import { TextareaRenderable } from "@opentui/core"

const textarea = new TextareaRenderable(renderer, {
  width: 50,
  height: 6,
  onSubmit: () => {
    console.log("Submitted:", textarea.plainText)
  },
  keyBindings: [{ name: "return", ctrl: true, action: "submit" }],
})

Placeholder styling

const textarea = new TextareaRenderable(renderer, {
  width: 40,
  height: 4,
  placeholder: "Type here",
  placeholderColor: "#666666",
})

Properties

PropertyTypeDefaultDescription
widthnumber or string-Width in terminal columns or percentage
heightnumber or string-Height in rows or percentage
initialValuestring""Initial text content
placeholderstring, StyledText, or nullnullPlaceholder content
placeholderColorstring or RGBA#666666Placeholder color
backgroundColorstring or RGBAtransparentBackground when unfocused
textColorstring or RGBA#FFFFFFText color when unfocused
focusedBackgroundColorstring or RGBAinitial base colorBackground when focused
focusedTextColorstring or RGBAinitial base colorText color when focused
wrapMode"none", "char", or "word""word"Line wrapping mode
selectionBgstring or RGBA-Selection background
selectionFgstring or RGBA-Selection foreground
cursorColorstring or RGBA#FFFFFFCursor color
cursorStyleCursorStyleOptions-Cursor style and blinking
selectionOccupancy"cell" or "boundary""cell"Which cells a selection occupies
keyBindingsKeyBinding[]-Custom key bindings
keyAliasMapRecord<string, string>-Key alias mapping
onSubmit(event: SubmitEvent) => void-Submit handler
onContentChange(event: ContentChangeEvent) => void-Fired on content changes
onCursorChange(event: CursorChangeEvent) => void-Fired on cursor movement

If you omit a focused color, the constructor copies the corresponding base color. If you omit both values, the focused background is transparent, and the focused text uses #FFFFFF.

Useful properties

PropertyTypeDescription
plainTextstringCurrent text content
cursorOffsetnumberCursor offset in the buffer
cursorCharacterOffsetnumber | undefinedBest-effort UTF-16 index of the character under the cursor
logicalCursor{ row, col }Logical line/column of the cursor
visualCursorVisualCursorVisual and logical cursor coordinates, plus the buffer offset
traitsEditorTraitsEditor traits published to hosting UI (see Traits)

cursorCharacterOffset uses a display-cell offset as a JavaScript string index. Its result is not reliable after wide graphemes, line breaks, or joined emoji. Use logicalCursor, visualCursor, and the editing-buffer APIs for Unicode-aware work.

Cursor and selection control

TextareaRenderable and its base EditBufferRenderable expose a programmatic API. You can move the cursor, edit text, and drive selections from your own key bindings or commands. All selection-aware movement methods accept { select: true } to extend the current selection instead of moving the cursor.

Cursor movement

textarea.setCursor(row, col)
textarea.moveCursorLeft()
textarea.moveCursorRight({ select: true })
textarea.moveCursorUp()
textarea.moveCursorDown()

textarea.moveWordForward({ select: true })
textarea.moveWordBackward()

textarea.gotoLine(0)
textarea.gotoLineStart()
textarea.gotoLineTextEnd()
textarea.gotoLineHome({ select: true }) // Emacs-style smart home
textarea.gotoLineEnd()
textarea.gotoVisualLineHome()
textarea.gotoVisualLineEnd()
textarea.gotoBufferHome()
textarea.gotoBufferEnd({ select: true })

Selection

Textarea uses the repeated-click behavior from Text selection. After a double-click or triple-click, the cursor stays on the clicked grapheme. A later Shift+Arrow keeps the selected text and continues the selection by cells, not by words or lines.

textarea.setSelection(start, end) // half-open [start, end) in both occupancy modes
textarea.setSelectionInclusive(start, end) // also selects the grapheme at end in cell mode
textarea.selectAll()
textarea.clearSelection()
textarea.deleteSelection()

Selecting keyboard focus in a draft:

Draft
Plan the release
Review keyboard focus
Ship the update

Editing

textarea.insertChar("a")
textarea.insertText("\ninserted")
textarea.deleteChar() // forward delete
textarea.deleteCharBackward() // backspace
textarea.deleteWordForward()
textarea.deleteWordBackward()
textarea.deleteToLineEnd()
textarea.deleteToLineStart()
textarea.deleteLine()
textarea.newLine()
textarea.undo()
textarea.redo()

These methods update the editor and request a render as needed. Selection behavior depends on the method. Movement with { select: true } extends the selection. Call clearSelection() when a command must clear the global selection.

The default occupancy is cell: the selection covers both endpoint cells, so the first shift+right selects two cells. If you use a bar cursor (cursorStyle: { style: "line" }), also set selectionOccupancy: "boundary". The cursor style is visual only and never changes which text you select, copy, or delete.

Traits

The traits property tells a host UI which built-in keys the editor wants to capture. It also supplies a visual-suspension hint and an optional status label. Assigning a different EditorTraits object emits the traits-changed event.

import { EditBufferRenderableEvents, type EditorTraits } from "@opentui/core"

textarea.traits = {
  capture: ["escape", "submit"], // consume these before host binds
  suspend: false,
  status: "Composing reply",
} satisfies EditorTraits

textarea.on(EditBufferRenderableEvents.TRAITS_CHANGED, (traits) => {
  updateFooter(traits.status ?? "")
})
FieldTypeDescription
captureEditorCapture[]Keys the editor wants to capture: "escape", "navigate", "submit", "tab"
suspendbooleanHint to the host to suspend ambient UI (dim borders, hide hints, etc.)
statusstringOptional short label surfacing editor mode in a status bar

Traits reset to an empty object when you destroy the renderable. Use isEditBufferRenderable(renderable) if you need to distinguish editor renderables from plain text renderables in a generic tree.

Read Interaction, focus, and selection for focus and selection ownership. Read Text and terminal cells for the difference between buffer offsets, graphemes, and display cells.