{"id":50373664,"url":"https://github.com/omranjamal/react-ink-textarea","last_synced_at":"2026-05-30T08:30:24.285Z","repository":{"id":355696872,"uuid":"1227316873","full_name":"omranjamal/react-ink-textarea","owner":"omranjamal","description":"Full featured batteries included CLI textarea component for React Ink","archived":false,"fork":false,"pushed_at":"2026-05-04T20:14:00.000Z","size":274,"stargazers_count":3,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-05-04T22:10:01.142Z","etag":null,"topics":["cli","ink","react","reactjs","reactjs-components"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/omranjamal.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-02T14:13:01.000Z","updated_at":"2026-05-04T21:55:44.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/omranjamal/react-ink-textarea","commit_stats":null,"previous_names":["omranjamal/ink-textarea"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/omranjamal/react-ink-textarea","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/omranjamal%2Freact-ink-textarea","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/omranjamal%2Freact-ink-textarea/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/omranjamal%2Freact-ink-textarea/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/omranjamal%2Freact-ink-textarea/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/omranjamal","download_url":"https://codeload.github.com/omranjamal/react-ink-textarea/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/omranjamal%2Freact-ink-textarea/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33686018,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-30T02:00:06.278Z","response_time":92,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["cli","ink","react","reactjs","reactjs-components"],"created_at":"2026-05-30T08:30:22.515Z","updated_at":"2026-05-30T08:30:24.271Z","avatar_url":"https://github.com/omranjamal.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# react-ink-textarea\n\u003e A multiline textarea component for [Ink](https://github.com/vadimdemedes/ink)\n\n[\u003cimg alt=\"GitHub Issues or Pull Requests\" src=\"https://img.shields.io/github/issues/omranjamal/react-ink-textarea\"\u003e](https://github.com/omranjamal/react-ink-textarea/issues) [\u003cimg alt=\"NPM Downloads\" src=\"https://img.shields.io/npm/dw/react-ink-textarea\"\u003e](https://www.npmjs.com/package/react-ink-textarea) [\u003cimg alt=\"NPM Version\" src=\"https://img.shields.io/npm/v/react-ink-textarea\"\u003e](https://www.npmjs.com/package/react-ink-textarea) [\u003cimg alt=\"NPM License\" src=\"https://img.shields.io/npm/l/react-ink-textarea\"\u003e](https://github.com/omranjamal/react-ink-textarea/blob/main/LICENSE) [\u003cimg alt=\"GitHub forks\" src=\"https://img.shields.io/github/forks/omranjamal/react-ink-textarea?style=flat\"\u003e](https://github.com/omranjamal/react-ink-textarea/network/members)\n\n\n\n\nBuild rich CLI forms with a full-featured textarea that supports multi-line editing, cursor navigation, undo, and customizable line prefixes.\n\n\u003cimg width=\"580\" alt=\"react-ink-textarea demo\" src=\"https://github.com/user-attachments/assets/fbd7325e-4b16-4fde-96f6-e02a1b839cb9\" /\u003e\n\n\n## Contents\n\n- [Features](#features)\n- [Install](#install)\n- [Usage](#usage)\n  - [1. Basic](#1-basic)\n  - [2. Controlled mode with cursor labels](#2-controlled-mode-with-cursor-labels)\n  - [3. Line numbers and active-line highlight](#3-line-numbers-and-active-line-highlight)\n  - [4. Inline syntax highlighting](#4-inline-syntax-highlighting)\n  - [5. Multi-field form with focus chaining](#5-multi-field-form-with-focus-chaining)\n  - [6. Slash-command picker (arrow + tab handoff)](#6-slash-command-picker-arrow--tab-handoff)\n  - [7. Code-editor preset](#7-code-editor-preset)\n- [Props](#props)\n- [Imperative API (ref)](#imperative-api-ref)\n- [Keybindings](#keybindings)\n  - [Keybinding Toggles](#keybinding-toggles)\n- [Caveats \u0026 limitations](#caveats--limitations)\n- [Development](#development)\n- [License](#license)\n\n## Features\n\n- 🎨 Polished feel — blinking cursor that pauses while typing, active-line highlight, multi-line placeholder, optional whitespace glyphs.\n- 🪪 Custom gutter via `linePrefix` render-prop, plus a drop-in `\u003cLineNumberPrefix /\u003e`.\n- 🌈 Regex (or function) labels with per-label styles; cursor reports the label under it.\n- ⌨️ Readline keybindings, configurable per chord. `Tab` is a callback. Grouped undo and bracketed paste.\n- 🌐 Unicode-correct: grapheme cursor, visual-width wrapping, real tab expansion, CRLF normalized.\n- 📐 Built-in viewport virtualization; auto-scroll; resize-aware.\n- 🧭 Boundary callbacks (`onFirstLineUp`, `onLastLineDown`, `onFirstCharacterLeft`, `onLastCharacterRight`) for parent-owned focus chaining.\n- ⚛️ Controlled, uncontrolled, or mixed. Imperative `ref.insert(text)` for autocomplete pickers.\n- 🧷 Strict TypeScript, tree-shakable.\n- 🧪 Works with [`ink-testing-library`](https://github.com/vadimdemedes/ink-testing-library); 250+ tests in-repo.\n\n## Install\n\nInstall [react-ink-textarea](https://www.npmjs.com/package/react-ink-textarea) via your JS package manager of choice.\n\n```bash\nnpm install react-ink-textarea\n\n# or\npnpm add react-ink-textarea\n```\n\n## Usage\n\n### 1. Basic\n\nUncontrolled mode. Submit on Enter, freeze on submit.\n\n```tsx\nimport { render } from \"ink\";\nimport { useState } from \"react\";\nimport { TextArea } from \"react-ink-textarea\";\n\nconst App = () =\u003e {\n  const [submitted, setSubmitted] = useState(\"\");\n  const [focus, setFocus] = useState(true);\n\n  return (\n    \u003cTextArea\n      focus={focus}\n      placeholder=\"Type your message here...\"\n      onSubmit={(value) =\u003e {\n        setSubmitted(value);\n        setFocus(false);\n      }}\n    /\u003e\n  );\n};\n\nrender(\u003cApp /\u003e);\n```\n\n### 2. Controlled mode with cursor labels\n\nOwn `value` and `cursorPosition` externally. `onCursorChange` reports the label and chunk index under the cursor — drive a status line, hover-card, or autocomplete from it.\n\n```tsx\nimport { Box, Text } from \"ink\";\nimport { useState } from \"react\";\nimport { TextArea, type TLabels } from \"react-ink-textarea\";\n\nconst labels: TLabels = [{ pattern: /#\\w+/g, label: \"tag\" }];\nconst styles = { tag: { color: \"magenta\" } };\n\nconst Editor = () =\u003e {\n  const [value, setValue] = useState(\"hello #world\");\n  const [cursor, setCursor] = useState\u003c[number, number]\u003e([0, 0]);\n  const [info, setInfo] = useState(\"text\");\n\n  return (\n    \u003cBox flexDirection=\"column\"\u003e\n      \u003cTextArea\n        focus\n        value={value}\n        cursorPosition={cursor}\n        labels={labels}\n        styles={styles}\n        onChange={setValue}\n        // (pos, labelType, chunkIndex) — labelType is \"text\" outside any match\n        onCursorChange={(pos, type, idx) =\u003e {\n          setCursor(pos);\n          setInfo(type === \"text\" ? \"text\" : `${type}#${idx}`);\n        }}\n        onSubmit={() =\u003e {}}\n      /\u003e\n      \u003cText dimColor\u003eunder cursor: {info}\u003c/Text\u003e\n    \u003c/Box\u003e\n  );\n};\n```\n\n### 3. Line numbers and active-line highlight\n\nDrop-in: pass the bundled `LineNumberPrefix` straight to `linePrefix`.\n\n```tsx\nimport { TextArea, LineNumberPrefix } from \"react-ink-textarea\";\n\n\u003cTextArea\n  focus\n  highlightActiveLine\n  activeLineColor=\"#222\"\n  initialLineCount={6}\n  onSubmit={() =\u003e {}}\n  linePrefix={LineNumberPrefix}\n/\u003e;\n```\n\nCustom: `linePrefix` is a render prop. Handle `isContinuationLine` and `isVirtualLine` to draw clean gutters when wrapping or padding. Compose with the bundled `LineNumber` for the digits.\n\n```tsx\nimport { Text } from \"ink\";\nimport { TextArea, LineNumber } from \"react-ink-textarea\";\n\n\u003cTextArea\n  focus\n  highlightActiveLine\n  initialLineCount={6}\n  onSubmit={() =\u003e {}}\n  linePrefix={({\n    lineNumber,\n    totalLines,\n    isActiveLine,\n    isContinuationLine,\n    isVirtualLine,\n  }) =\u003e\n    isContinuationLine ? (\n      \u003cText color=\"gray\"\u003e  ↳ \u003c/Text\u003e\n    ) : (\n      \u003cText\u003e\n        \u003cLineNumber\n          lineNumber={lineNumber}\n          totalLines={totalLines}\n          isActive={isActiveLine}\n          padChar=\" \"\n          activeColor=\"cyan\"\n        /\u003e\n        \u003cText color={isVirtualLine ? \"gray\" : \"white\"}\u003e │ \u003c/Text\u003e\n      \u003c/Text\u003e\n    )\n  }\n/\u003e;\n```\n\n### 4. Inline syntax highlighting\n\n`labels` mixes regex rules with function-form rules for allowlists. First rule wins on overlap. `styles.text` and `styles.invisibleCharacter` are reserved keys; everything else maps to a label name.\n\n```tsx\nimport { useMemo } from \"react\";\nimport { TextArea, type TLabels } from \"react-ink-textarea\";\n\nconst KNOWN_USERS = new Set([\"alice\", \"bob\", \"carol\"]);\n\nconst Editor = () =\u003e {\n  const labels = useMemo\u003cTLabels\u003e(\n    () =\u003e [\n      { pattern: /https?:\\/\\/\\S+/g, label: \"url\" },\n      { pattern: /#\\w+/g, label: \"tag\" },\n      // function form: leave unknown @handles unstyled by returning undefined\n      {\n        pattern: /@(\\w+)/g,\n        label: (m) =\u003e (KNOWN_USERS.has(m[1]) ? \"mention\" : undefined),\n      },\n    ],\n    [],\n  );\n\n  return (\n    \u003cTextArea\n      focus\n      onSubmit={() =\u003e {}}\n      showInvisibles={{ space: false, tab: true, newline: false }}\n      labels={labels}\n      styles={{\n        text: { color: \"white\" },\n        invisibleCharacter: { color: \"gray\", dim: true },\n        url: { color: \"blue\", underline: true },\n        tag: { color: \"magenta\" },\n        mention: { color: \"green\", bold: true },\n      }}\n    /\u003e\n  );\n};\n```\n\n### 5. Multi-field form with focus chaining\n\nBoundary callbacks let you escape the textarea cleanly: ↑ on the first row jumps to the field above, ↓ past the last line jumps below, and ←/→ at the absolute ends do the same horizontally.\n\n```tsx\nimport { Box, useFocusManager, useFocus } from \"ink\";\nimport { useState } from \"react\";\nimport { TextArea } from \"react-ink-textarea\";\nimport TextInput from \"ink-text-input\";\n\nconst Form = () =\u003e {\n  const { focusNext, focusPrevious } = useFocusManager();\n  const subject = useFocus({ id: \"subject\" });\n  const body = useFocus({ id: \"body\" });\n  const tags = useFocus({ id: \"tags\" });\n  const [s, setS] = useState(\"\");\n  const [b, setB] = useState(\"\");\n  const [t, setT] = useState(\"\");\n\n  return (\n    \u003cBox flexDirection=\"column\" gap={1}\u003e\n      \u003cTextInput value={s} onChange={setS} focus={subject.isFocused} /\u003e\n      \u003cTextArea\n        focus={body.isFocused}\n        value={b}\n        onChange={setB}\n        onSubmit={() =\u003e {}}\n        onFirstLineUp={focusPrevious}\n        onLastLineDown={focusNext}\n        onFirstCharacterLeft={focusPrevious}\n        onLastCharacterRight={focusNext}\n      /\u003e\n      \u003cTextInput value={t} onChange={setT} focus={tags.isFocused} /\u003e\n    \u003c/Box\u003e\n  );\n};\n```\n\n### 6. Slash-command picker (arrow + tab handoff)\n\nWhen a menu opens, suspend cursor navigation with `disableArrowNavigation` and disable `Enter` so the picker — not the textarea — handles them. Re-enable on close.\n\n```tsx\nimport { Box, Text } from \"ink\";\nimport { useState } from \"react\";\nimport { TextArea } from \"react-ink-textarea\";\n\nconst COMMANDS = [\"/help\", \"/quit\", \"/train\"];\n\nconst Composer = () =\u003e {\n  const [value, setValue] = useState(\"\");\n  const [sel, setSel] = useState(0);\n  const open = value.startsWith(\"/\");\n\n  return (\n    \u003cBox flexDirection=\"column\"\u003e\n      \u003cTextArea\n        focus\n        value={value}\n        onChange={setValue}\n        onSubmit={(v) =\u003e {\n          if (!open) console.log(v);\n        }}\n        // While the picker is open, give it the arrows + Enter; typing still flows.\n        disableArrowNavigation={open}\n        keybindings={open ? { Enter: false } : undefined}\n        onTab={(shift) =\u003e\n          setSel((i) =\u003e (i + (shift ? -1 : 1) + COMMANDS.length) % COMMANDS.length)\n        }\n      /\u003e\n      {open \u0026\u0026 (\n        \u003cBox flexDirection=\"column\"\u003e\n          {COMMANDS.map((c, i) =\u003e (\n            \u003cText key={c} color={i === sel ? \"cyan\" : undefined}\u003e\n              {i === sel ? \"▸ \" : \"  \"}\n              {c}\n            \u003c/Text\u003e\n          ))}\n        \u003c/Box\u003e\n      )}\n    \u003c/Box\u003e\n  );\n};\n```\n\n### 7. Code-editor preset\n\nOverride the viewport explicitly, expand tabs, lock down ergonomic chords, and surface the measured content width to a status bar.\n\n```tsx\nimport { Box, Text } from \"ink\";\nimport { useState } from \"react\";\nimport { TextArea } from \"react-ink-textarea\";\n\nconst CodeEditor = () =\u003e {\n  const [width, setWidth] = useState(0);\n\n  return (\n    \u003cBox flexDirection=\"column\"\u003e\n      \u003cTextArea\n        focus\n        onSubmit={() =\u003e {}}\n        viewportLines={20}\n        tabWidth={2}\n        autoNewLineLimit={1}\n        showInvisibles\n        keybindings={{\n          // Single-undo-stack ergonomics: defer undo to the host (e.g. file-level)\n          \"Ctrl+Z\": false,\n          // Keep Shift+Enter free for the parent's \"submit-with-newline\" gesture\n          \"Shift+Enter\": false,\n        }}\n        onDimensions={setWidth}\n      /\u003e\n      \u003cText dimColor\u003ewidth: {width} cols\u003c/Text\u003e\n    \u003c/Box\u003e\n  );\n};\n```\n\n## Props\n\n| Prop                    | Type                                                                                        | Description                                                                                                                               |\n| ----------------------- | ------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |\n| `focus`                 | `boolean`                                                                                   | Whether the textarea is focused and receiving keyboard input.                                                                             |\n| `onSubmit`              | `(value: string) =\u003e void`                                                                   | Called when the user presses **Enter**. Receives the full text.                                                                           |\n| `placeholder`           | `string`                                                                                    | Placeholder text shown when the textarea is empty.                                                                                        |\n| `linePrefix`            | `ReactNode \\| (props: TLinePrefixProps) =\u003e ReactNode`                                       | Optional prefix rendered before each line. The function form receives `{ lineNumber, totalLines, isActiveLine, isVirtualLine, isContinuationLine, continuationIndex }`. Use for line numbers, gutters, borders, etc. |\n| `highlightActiveLine`   | `boolean`                                                                                   | When `true`, the active line is highlighted with a subtle background color. Defaults to `false`.                                          |\n| `activeLineColor`       | `string`                                                                                    | Background color for the active line highlight. Defaults to no color.                                                                     |\n| `cursorInterval`        | `number`                                                                                    | Cursor blink interval in milliseconds. Defaults to `500`.                                                                                 |\n| `typingPause`           | `number`                                                                                    | Milliseconds to wait after typing before resuming cursor blink. Defaults to `450`.                                                        |\n| `maxUndo`               | `number`                                                                                    | Maximum number of undo steps to retain. Defaults to `128`.                                                                                |\n| `undoGroupDelay`        | `number`                                                                                    | Milliseconds to group consecutive edits into a single undo step. Defaults to `750`.                                                       |\n| `autoNewLineLimit`      | `number`                                                                                    | Maximum number of empty lines allowed after the last line with content. Only applies to Down arrow navigation. Defaults to `3`.           |\n| `disableArrowNavigation` | `boolean`                                                                                  | When `true`, disables cursor movement via arrow keys (and word/line jumps). Useful for implementing suggestion pickers. Defaults to `false`.                    |\n| `keybindings`           | `Partial\u003cRecord\u003cTKeybinding, boolean\u003e\u003e`                                                     | Per-chord enable/disable map. Merged over defaults (all `true`). Set a chord to `false` to swallow it. `disableArrowNavigation: true` additionally forces all nav chords off. See **Keybinding Toggles** below.        |\n| `initialLineCount`      | `number`                                                                                    | Number of lines to display initially. The textarea will maintain at least this many lines. Defaults to `2`.                               |\n| `viewportLines`         | `number`                                                                                    | Maximum number of visual rows rendered at once. The textarea virtualizes rendering and auto-scrolls to keep the cursor visible. Defaults to `floor(stdout.rows * 0.5)` so blink re-renders don't scroll-jank tall buffers when the frame exceeds the terminal viewport. Pass an explicit number to override; `Infinity` renders every row. |\n| `tabWidth`              | `number`                                                                                    | Visual width of `\\t` characters in cells. Tabs render as `tabWidth` spaces (or `→` + spaces with `showInvisibles.tab`). The stored value keeps `\\t`. Defaults to `4`. |\n| `value`                 | `string`                                                                                    | **Controlled mode**: The current value of the textarea. When provided, component operates in controlled mode.                             |\n| `cursorPosition`        | `[line: number, column: number]`                                                            | **Controlled mode**: The current cursor position as a `[line, column]` tuple. Use with `value` for full control.                          |\n| `onChange`              | `(value: string) =\u003e void`                                                                   | **Controlled mode**: Called when the value changes.                                                                                       |\n| `onCursorChange`        | `(position: [line, column], type: string, chunkIndex: number) =\u003e void`                      | **Controlled mode**: Called when the cursor moves. `type` is the label at the cursor (`\"text\"` if no label matches); `chunkIndex` is the zero-based index of the labeled segment the cursor is in. |\n| `onFirstLineUp`         | `() =\u003e void`                                                                                | Called when Up arrow is pressed on the first visual row. Useful for moving focus out of the textarea.                                     |\n| `onLastLineDown`        | `() =\u003e void`                                                                                | Called when Down arrow is pressed on the last line and trailing-empty-line limit is reached. Useful for moving focus out.                 |\n| `onFirstCharacterLeft`  | `() =\u003e void`                                                                                | Called when Left arrow is pressed at the very start of the value (`cursor === 0`). Useful for moving focus to a previous field.            |\n| `onLastCharacterRight`  | `() =\u003e void`                                                                                | Called when Right arrow is pressed at the very end of the value (`cursor === value.length`). Useful for moving focus to a next field.       |\n| `onTab`                 | `(shift: boolean) =\u003e void`                                                                  | Called when Tab is pressed. `shift` is `true` for Shift+Tab. Without this prop, Tab is silently swallowed (no value mutation).            |\n| `onDimensions`          | `(width: number) =\u003e void`                                                                   | Called with the measured content width whenever it changes.                                                                               |\n| `showInvisibles`        | `boolean \\| { space?: boolean; tab?: boolean; newline?: boolean }`                          | Render whitespace glyphs (`·` for space, `→` for tab, `↵` for newline). Defaults to `false`.                                              |\n| `styles`                | `{ text?, invisibleCharacter?, [labelName]? }` of `TStyleProps`                             | Style overrides for the default text run, invisible glyphs, and any user-defined labels. `color` and `bgColor` accept any value Ink's `\u003cText\u003e` accepts — see the [Ink color reference](https://github.com/vadimdemedes/ink#color). |\n| `labels`                | `readonly { pattern: RegExp; label: string \\| ((match: RegExpMatchArray) =\u003e string \\| undefined) }[]` | Array of label rules. Each rule's `pattern` is matched against the value; matches receive the rule's `label`. Use a function form to allowlist matches — return `undefined` to leave a match unlabeled. First rule wins on overlap. |\n\n## Imperative API (ref)\n\nPass a `ref` of type `TextAreaHandle` to insert text programmatically — typically from an autocomplete picker. Insertion happens at the current cursor and advances it. Works in both controlled and uncontrolled modes.\n\n```tsx\nimport { useRef } from \"react\";\nimport { TextArea, type TextAreaHandle } from \"react-ink-textarea\";\n\nconst Composer = () =\u003e {\n  const ref = useRef\u003cTextAreaHandle\u003e(null);\n  return (\n    \u003cTextArea\n      ref={ref}\n      focus\n      onSubmit={() =\u003e {}}\n      onTab={() =\u003e ref.current?.insert(\"/help \")}\n    /\u003e\n  );\n};\n```\n\n| Method                  | Description                                                                                  |\n| ----------------------- | -------------------------------------------------------------------------------------------- |\n| `insert(text: string)`  | Insert `text` at the current cursor and advance it past the inserted text. Empty string is a no-op. |\n\n## Keybindings\n\n| Key             | Action                         |\n| --------------- | ------------------------------ |\n| `Ctrl+J`        | Insert newline                 |\n| `Ctrl+Enter`    | Insert newline                 |\n| `Shift+Enter`   | Insert newline                 |\n| `Alt+Enter`     | Insert newline (Option+Enter)  |\n| `Enter`         | Submit                         |\n| `↑` / `↓`       | Move cursor between lines      |\n| `←` / `→`       | Move cursor left / right       |\n| `Opt+←`         | Jump to previous word          |\n| `Opt+→`         | Jump to next word              |\n| `Ctrl+A`        | Start of current line          |\n| `Ctrl+E`        | End of current line            |\n| `Ctrl+W`        | Delete word before cursor      |\n| `Ctrl+U`        | Delete to start of line. At column 0, joins with previous line (matches Cmd+Backspace mapping in iTerm2/ghostty). |\n| `Ctrl+K`        | Delete to end of line          |\n| `Backspace`     | Delete character before cursor |\n| `Delete`        | Delete character before cursor (same as `Backspace`) |\n| `Opt+Backspace` | Delete word before cursor      |\n| `Ctrl+Z`        | Undo (up to 128 steps)         |\n\n\u003e On macOS, `Alt` chords are pressed via the **Option** (`⌥`) key.\n\n### Keybinding Toggles\n\nPass a `keybindings` map to disable individual chords. Keys are the chord strings themselves; values are `true` (enabled) or `false` (disabled). Anything you don't list defaults to enabled.\n\n```tsx\n\u003cTextArea\n  focus\n  onSubmit={onSubmit}\n  keybindings={{\n    \"Ctrl+Z\": false,      // disable undo\n    \"Shift+Enter\": false, // disable Shift+Enter newline (other newline chords still work)\n    \"Alt+B\": false,       // disable previous-word jump\n  }}\n/\u003e\n```\n\nThe full chord catalog (every key is a `TKeybinding`):\n\n| Chord            | Action                            |\n| ---------------- | --------------------------------- |\n| `Enter`          | Submit                            |\n| `Ctrl+J`         | Insert newline                    |\n| `Ctrl+Enter`     | Insert newline                    |\n| `Shift+Enter`    | Insert newline                    |\n| `Alt+Enter`      | Insert newline                    |\n| `Up`             | Cursor up                         |\n| `Down`           | Cursor down                       |\n| `Left`           | Cursor left                       |\n| `Right`          | Cursor right                      |\n| `Alt+B`          | Previous word                     |\n| `Alt+F`          | Next word                         |\n| `Ctrl+A`         | Start of line                     |\n| `Ctrl+E`         | End of line                       |\n| `Ctrl+W`         | Delete word before cursor         |\n| `Ctrl+U`         | Delete to start of line           |\n| `Ctrl+K`         | Delete to end of line             |\n| `Backspace`      | Delete grapheme before cursor     |\n| `Delete`         | Delete grapheme before cursor     |\n| `Alt+Backspace`  | Delete word before cursor         |\n| `Ctrl+Z`         | Undo                              |\n\n`disableArrowNavigation: true` additionally forces all nav chords (`Up`, `Down`, `Left`, `Right`, `Alt+B`, `Alt+F`, `Ctrl+A`, `Ctrl+E`) off regardless of the map.\n\n## Caveats \u0026 limitations\n\nThings to know before shipping. Most are intrinsic to running a rich editor inside a terminal.\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eEnvironment\u003c/b\u003e\u003c/summary\u003e\n\n- **Terminal-only.** Built on Ink — won't render in a browser. Requires Node 18+ for `Intl.Segmenter`. Peer deps: `ink ^7`, `react ^18 || ^19`.\n- **Monospace assumption.** Layout assumes every cell is one column wide and CJK / wide chars take two. Variable-width fonts in some emulators (rare) will break alignment.\n- **Visual width via `string-width`.** Emoji + ZWJ widths follow Unicode tables; some terminals (notably older macOS Terminal, tmux without `set -g escape-time 0`) render the same glyph at a different cell count and produce off-by-one cursor placement.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eKeybinding edge cases\u003c/b\u003e\u003c/summary\u003e\n\n- **Modifier+Enter detection is terminal-dependent.** `Ctrl+Enter`, `Shift+Enter`, `Alt+Enter` rely on `modifyOtherKeys` / CSI-u sequences. macOS Terminal.app and Windows console don't emit them by default — use iTerm2, WezTerm, Kitty, or Alacritty, or fall back to `Ctrl+J` for newline.\n- **`Alt` on macOS.** Option key inserts special chars (`Opt+B` → `∫`) unless the terminal is set to \"Use Option as Meta\" (iTerm2: *Profiles → Keys*; Terminal.app: *Profiles → Keyboard → Use Option as Meta key*).\n- **`Tab` is silently swallowed without `onTab`.** No newline, no insert, no error. Provide the handler if you want any Tab behavior.\n- **`disableArrowNavigation` is not read-only.** Typing still mutates the buffer. Use `focus={false}` for true read-only.\n- **Multiple focused TextAreas race.** Ink's `useInput` delivers keys to every active hook; two textareas with `focus={true}` will both mutate. Gate via `focus` per instance.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003ePaste \u0026 clipboard\u003c/b\u003e\u003c/summary\u003e\n\n- **Bracketed paste required for one-step paste.** Terminals that don't emit `\\x1b[200~` (Windows cmd, very old emulators) deliver pastes as individual keystrokes — slow, and each char becomes its own undo step (effectively breaking `Ctrl+Z` for the paste).\n- **No programmatic clipboard.** No copy API; no setter for paste content. Anything the terminal doesn't deliver, the textarea can't see.\n- **No mouse.** Click-to-position, drag-select, scroll wheel — none of it. Cursor moves only via keys.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eSelection \u0026 search\u003c/b\u003e\u003c/summary\u003e\n\n- **No selection.** Point cursor only. No range, no shift+arrow extension, no copy of a range.\n- **No find/replace.** Build it on top via controlled mode if you need it.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eUndo\u003c/b\u003e\u003c/summary\u003e\n\n- **Time-grouped, not semantic.** Edits within `undoGroupDelay` (default 2.5 s) collapse into one step. On a slow machine the boundary may land mid-word.\n- **Bounded by `maxUndo`** (default 128). Older history is dropped silently.\n- **No redo.** `Ctrl+Y` / `Ctrl+Shift+Z` are not bound. Add yourself if needed.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eLabels \u0026 styles\u003c/b\u003e\u003c/summary\u003e\n\n- **Regex runs on every value change.** O(value × rules). Heavy patterns on 100k+ char buffers will lag — debounce externally or scope rules.\n- **Flat ranges, no nesting.** A label can't contain another label. First rule wins on overlap; later rules silently lose.\n- **Function-form `undefined` ≠ \"fall through\".** Returning `undefined` leaves the match unlabeled (renders with `text` style). It does not let the next rule try.\n- **`color` / `bgColor` strings are passed straight to Ink's `\u003cText\u003e`.** Invalid values fail silently — the terminal renders default. See the [Ink color reference](https://github.com/vadimdemedes/ink#color).\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003ePerformance\u003c/b\u003e\u003c/summary\u003e\n\n- **Cursor blink causes re-renders every `cursorInterval` ms (default 500).** Tall frames + slow terminals can flicker. Bump `cursorInterval` or set it high to effectively disable.\n- **`viewportLines` defaults to `floor(stdout.rows * 0.5)`.** Without virtualization, frames taller than the terminal trigger scroll-jank on every blink. The default trades render area for stability — override explicitly if you have height to spare.\n- **Visual-row recompute is O(value)** per keystroke. Acceptable for chat/comment buffers; pathological for full-file editing of large source files.\n- **Resize listener is global.** Every TextArea instance subscribes to `stdout.resize`. Dozens of simultaneous instances will fan out resize events.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eData model\u003c/b\u003e\u003c/summary\u003e\n\n- **CRLF/CR normalized to LF on paste and controlled values.** `onChange` always reports LF. If your storage layer needs CRLF, convert on save.\n- **`value.length` ≠ visual length.** Tabs count as 1 char regardless of `tabWidth`; emoji count as their UTF-16 code-unit length, not 1 grapheme. Use `Intl.Segmenter` if you need grapheme counts externally.\n- **Out-of-bounds `cursorPosition` is clamped silently.** No throw, no warning — `onCursorChange` reports the clamped value.\n- **Boundary callbacks fire on exact bounds only.** `onFirstLineUp` only fires when the cursor is *on* row 0 and ↑ is pressed; not \"the cursor moved past the top.\" Same for the other three.\n- **`onSubmit` is sync.** No promise return contract — coordinate async validation in your parent component.\n\n\u003c/details\u003e\n\n\u003cdetails\u003e\n\u003csummary\u003e\u003cb\u003eVirtualization gotchas\u003c/b\u003e\u003c/summary\u003e\n\n- **Rows outside `viewportLines` are not rendered.** Any consumer-side measurement on hidden rows (`useBoxMetrics`, refs in `linePrefix`) won't fire until the row scrolls in.\n- **Wrapping happens at the measured content width.** Constrain via a parent `\u003cBox width={...}\u003e` to wrap at a fixed column; otherwise wraps at `stdout.columns`.\n\n\u003c/details\u003e\n\n## Development\n\n```bash\n# Install dependencies\npnpm install\n\n# Build\npnpm run build\n\n# Watch mode\npnpm run dev\n\n# Run tests\npnpm test\n\n# Format code\npnpm run format\n```\n\n## License\n\nMIT\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fomranjamal%2Freact-ink-textarea","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fomranjamal%2Freact-ink-textarea","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fomranjamal%2Freact-ink-textarea/lists"}