Textarea
Textarea edits multiple lines with cursor movement, selection, and configurable key bindings. Use Input for a single line.
Availability#
| Field | Availability |
|---|---|
| Package | @opentui/core |
| Core renderable | TextareaRenderable |
| React | <textarea> (automatic) |
| Solid | <textarea> (automatic) |
| Status | Built 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.")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#
| Property | Type | Default | Description |
|---|---|---|---|
width |
number or string |
- | Width in terminal columns or percentage |
height |
number or string |
- | Height in rows or percentage |
initialValue |
string |
"" |
Initial text content |
placeholder |
string, StyledText, or null |
null |
Placeholder content |
placeholderColor |
string or RGBA |
#666666 |
Placeholder color |
backgroundColor |
string or RGBA |
transparent | Background when unfocused |
textColor |
string or RGBA |
#FFFFFF |
Text color when unfocused |
focusedBackgroundColor |
string or RGBA |
initial base color | Background when focused |
focusedTextColor |
string or RGBA |
initial base color | Text color when focused |
wrapMode |
"none", "char", or "word" |
"word" |
Line wrapping mode |
selectionBg |
string or RGBA |
- | Selection background |
selectionFg |
string or RGBA |
- | Selection foreground |
cursorColor |
string or RGBA |
#FFFFFF |
Cursor color |
cursorStyle |
CursorStyleOptions |
- | Cursor style and blinking |
selectionOccupancy |
"cell" or "boundary" |
"cell" |
Which cells a selection occupies |
keyBindings |
KeyBinding[] |
- | Custom key bindings |
keyAliasMap |
Record<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#
| Property | Type | Description |
|---|---|---|
plainText |
string |
Current text content |
cursorOffset |
number |
Cursor offset in the buffer |
cursorCharacterOffset |
number | undefined |
Best-effort UTF-16 index of the character under the cursor |
logicalCursor |
{ row, col } |
Logical line/column of the cursor |
visualCursor |
VisualCursor |
Visual and logical cursor coordinates, plus the buffer offset |
traits |
EditorTraits |
Editor 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:
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 ?? "")
})| Field | Type | Description |
|---|---|---|
capture |
EditorCapture[] |
Keys the editor wants to capture: "escape", "navigate", "submit", "tab" |
suspend |
boolean |
Hint to the host to suspend ambient UI (dim borders, hide hints, etc.) |
status |
string |
Optional 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.
Related concepts#
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.