Skip to content

TextArea

Multi-line text input with word wrapping, scrolling, selection, and cursor movement. Uses useBoxRect for width-aware word wrapping and scroll tracking to keep the cursor visible. Built on the useTextArea hook.

Import

tsx
import { TextArea } from "silvery"

Props

PropTypeDefaultDescription
valuestring--Current value (controlled mode)
defaultValuestring""Initial value (uncontrolled mode)
onChange(value: string) => void--Called when value changes
onSubmit(value: string) => void--Called on submit
submitKey"ctrl+enter" | "enter" | "meta+enter""ctrl+enter"Key to trigger submit
placeholderstring""Placeholder text when empty
isActiveboolean--Whether input is focused/active (overrides focus system)
fieldSizing"content" | "fixed""content"CSS field-sizing analog — auto-grow with content vs fixed
rowsnumber1Visible row count in "fixed" mode
minRowsnumber1Minimum rows in "content" mode
maxRowsnumber8Maximum rows in "content" mode (scrolls beyond)
cursorStyle"block" | "underline""block"Cursor style
scrollMarginnumber1Context lines above/below cursor when scrolling
disabledboolean--Ignore all input and dim text
maxLengthnumber--Maximum number of characters allowed
borderStylestring--Border style (wraps input in bordered Box)
borderColorstring"$border-default"Border color when unfocused
focusBorderColorstring"$border-focus"Border color when focused
testIDstring--Test ID for focus system identification
onEdge(edge) => boolean--Fires when arrow key pressed AT buffer boundary

Ref: TextAreaHandle

ts
interface TextAreaHandle {
  clear: () => void
  getValue: () => string
  setValue: (value: string) => void
  /** Set cursor position. Clamped to value length, scrolls to keep visible. */
  setCursor: (offset: number) => void
  getSelection: () => TextAreaSelection | null
}

type TextAreaSelection = { start: number; end: number }

setCursor is useful when you replace value and want the cursor at a specific offset (e.g. cursor-at-start after a swap-and-handoff). setValue always places the cursor at the end; pair it with setCursor(0) to land at the start instead.

Keyboard Shortcuts

KeyAction
Arrow keysMove cursor (clears selection)
Shift+ArrowExtend selection
Shift+Home/EndSelect to line boundaries
Ctrl+Shift+ArrowWord-wise selection
Ctrl+ASelect all text
Ctrl+EEnd of line
Home/EndBeginning/end of line
Alt+B/FMove by word (wraps across lines)
Ctrl+W / Alt+BackspaceDelete word backwards (kill ring)
Alt+DDelete word forwards (kill ring)
Ctrl+KKill to end of line
Ctrl+UKill to beginning of line
Ctrl+YYank (paste from kill ring)
Alt+YCycle kill ring
Ctrl+TTranspose characters
PageUp/PageDownScroll by viewport height

Sizing — field-sizing API

TextArea mirrors the CSS field-sizing property. "content" (default) auto-grows with input clamped between minRows and maxRows; "fixed" keeps the widget at exactly rows regardless of content.

Old usageNew usage
<TextArea height={N} /><TextArea fieldSizing="fixed" rows={N} />
Hand-rolled height={Math.min(N, lines.length)}<TextArea maxRows={N} /> (default content mode)
Chat input where height tracked content<TextArea /> (defaults are chat-input)

Usage

tsx
const [value, setValue] = useState('')

// Defaults give chat-input behavior — auto-grows 1..8 rows.
<TextArea
  value={value}
  onChange={setValue}
  onSubmit={(val) => console.log('Submitted:', val)}
  placeholder="Type here..."
/>

// Fixed-height editor pane.
<TextArea
  value={value}
  onChange={setValue}
  fieldSizing="fixed"
  rows={10}
/>

Edge Callbacks: onEdge

onEdge fires when an arrow key is pressed AT the buffer boundary — where the key would otherwise be a no-op or clamp. It enables cross-widget focus handoff for composite editors that stack multiple TextAreas and want arrow keys to flow between them.

ts
type Edge = "top" | "bottom" | "left" | "right"
onEdge?: (edge: Edge) => boolean
EdgeFires when
"top"Up is pressed at cursorRow === 0
"bottom"Down is pressed at the last row
"left"Left is pressed at the start of the buffer (offset 0)
"right"Right is pressed at the end of the buffer
  • Return true to consume the key — the cursor stays put and the arrow event is fully handled.
  • Return false (or omit the handler) to fall through to the default clamp behavior.
  • Not fired when Shift is held — Shift+Arrow extends selection instead and is reserved for future use.

Example: two-pane composite editor

tsx
const [topValue, setTopValue] = useState("")
const [botValue, setBotValue] = useState("")
const [focused, setFocused] = useState<"top" | "bot">("top")

return (
  <Box flexDirection="column">
    <TextArea
      value={topValue}
      onChange={setTopValue}
      isActive={focused === "top"}
      fieldSizing="fixed"
      rows={5}
      onEdge={(edge) => {
        if (edge === "bottom") {
          setFocused("bot")
          return true // consume — focus moved to the lower pane
        }
        return false
      }}
    />
    <Divider />
    <TextArea
      value={botValue}
      onChange={setBotValue}
      isActive={focused === "bot"}
      fieldSizing="fixed"
      rows={5}
      onEdge={(edge) => {
        if (edge === "top") {
          setFocused("top")
          return true
        }
        return false
      }}
    />
  </Box>
)

See Also