Textarea

Text field that runs to several lines, sized in rows.

A field for text that runs to several lines. Its height is given in rows rather than in pixels, so it composes with size instead of fighting it — three rows of 14px text and three rows of 16px text are different heights, and both are three rows.

autoGrow then lets the field follow its content up to maxRows and scroll past that, which is what a composer wants: it starts small, opens up as you write, and stops before it eats the screen. Everything else it shares with Input — the animated focus border, the label, description and error line, and the keyboard avoidance.

Installation

Textarea ships with the library — no separate install.

import { Textarea, Card } from 'panelui-native';

Or copy the source into your project, to own and edit it:

npx panelui-cli@latest add textarea

Usage

<Textarea label="Notes" placeholder="Anything we should know?" />

<Textarea
  label="Bio"
  rows={3}
  description="Shown on your public profile."
/>

<Textarea
  autoGrow
  rows={2}
  maxRows={8}
  placeholder="Message"
/>

<Textarea
  label="Status"
  maxLength={280}
  showCount
  value={status}
  onChangeText={setStatus}
/>

Examples

Rows, not pixels

rows is how many lines of text the field is tall before it scrolls. It composes with size, so a small three-row field and a large three-row field are both three rows.

<Textarea rows={2} placeholder="Two rows" />
<Textarea rows={4} placeholder="Four rows" />
<Textarea size="sm" rows={4} placeholder="Four smaller rows" />

Growing with the text

autoGrow starts the field at rows and lets it follow the text up to maxRows, after which it holds its height and scrolls. It never shrinks below rows, so clearing the field returns it to where it started rather than collapsing it to one line.

<Textarea
  autoGrow
  rows={2}
  maxRows={6}
  placeholder="Type a few lines…"
  value={message}
  onChangeText={setMessage}
/>

Label, description and error

Setting errorMessage also puts the field into its invalid state — you do not need a separate flag. The description is replaced by the error while one is present.

<Textarea
  label="Bio"
  placeholder="A short bio"
  description="Shown on your public profile."
/>

<Textarea
  label="Feedback"
  isRequired
  value={feedback}
  onChangeText={setFeedback}
  errorMessage="Tell us a little more."
/>

Counting characters

showCount needs maxLength to count towards. The counter sits beside the description and turns destructive at the limit.

<Textarea
  label="Status"
  maxLength={280}
  showCount
  rows={3}
  description="Keep it short."
  value={status}
  onChangeText={setStatus}
/>

Choosing a background

Use outline on the page, where the field draws its own edge. Use filled inside a card or a sheet, where a second border beside the container’s own reads as a seam.

<Textarea label="Notes" placeholder="On the page" />

<Card>
  <Card.Content className="gap-4 p-4">
    <Textarea variant="filled" label="Notes" placeholder="In a card" />
  </Card.Content>
</Card>

Passing through TextInput props

Anything TextInput accepts works here, except multiline and numberOfLines — the component owns both.

<Textarea
  label="Address"
  rows={3}
  autoCapitalize="words"
  autoComplete="street-address"
  returnKeyType="done"
  submitBehavior="blurAndSubmit"
  onSubmitEditing={save}
/>

Versions

A composer that avoids the keyboard

A one-row composer that grows as the message does and rides the keyboard up. avoidKeyboard moves the field by exactly the amount the keyboard covers it, and autoGrow opens it up as the message gets longer — the pair of behaviours a chat input needs, and the one case a fixed box cannot show.

const insets = useSafeAreaInsets();

<KeyboardAvoider
  mode="dock"
  bottomInset={insets.bottom}
  className="absolute left-0 right-0 px-5"
  style={{ bottom: insets.bottom + 16 }}
>
  <View className="flex-row items-end gap-2 rounded-3xl border border-border bg-surface p-2">
    <Textarea
      autoGrow
      rows={1}
      maxRows={5}
      value={draft}
      onChangeText={setDraft}
      placeholder="Message"
      className="flex-1 border-0 bg-transparent px-2"
      containerClassName="flex-1"
    />
    <SendIcon size={18} />
  </View>
</KeyboardAvoider>

Variants

variant

  • outline (default)
  • filled
{/* On the page, where the field draws its own edge. */}
<Textarea variant="outline" label="Notes" />

{/* Inside a card or a sheet, where a second border reads as a seam. */}
<Textarea variant="filled" label="Notes" />

size

  • sm
  • md (default)
  • lg
<Textarea size="sm" rows={3} placeholder="14px type" />
<Textarea size="md" rows={3} placeholder="16px type" />
<Textarea size="lg" rows={3} placeholder="16px type, roomier" />

API Reference

Textarea

PropTypeDefaultDescription
classNamestring
containerClassNamestring
labelstring
descriptionstring
errorMessagestringError message. When set, the field renders in its invalid state.
isRequiredbooleanMarks the field required — an asterisk on the label, and the a11y state.
disabledboolean
rowsnumber4How many lines of text the field is tall before it scrolls or grows.
autoGrowbooleanfalseGrow with the text, one line at a time, up to maxRows — after which the field holds its height and scrolls instead. Without it the field stays at rows and scrolls from the first line past it.
maxRowsnumber10The tallest autoGrow will go. Ignored without it.
showCountbooleanfalseShow how much of maxLength is used, under the field. Needs maxLength; a counter with no limit to count towards says nothing.
avoidKeyboardbooleanfalseKeep the field clear of the software keyboard. Moves by exactly the overlap, and not at all when the field is already clear — or when the keyboard belongs to a different field. The overlap is re-read every frame while the field is focused, so the field keeps its place in the page as it scrolls under and back out of the keyboard. Install react-native-keyboard-controller for this to behave on Android. Do not toggle this at runtime — it changes which component renders the container, which would remount the field and drop focus.
keyboardModeKeyboardAvoidanceMode'lift'How the field gets clear. lift moves it up by its overlap and follows the scroll — right for a field in the flow of a page. dock makes it travel with the keyboard, for a composer already pinned near the bottom edge; pair it with keyboardBottomInset.
keyboardOffsetnumber16Gap kept between the field and the keyboard. keyboardMode="lift" only.
keyboardBottomInsetnumber0How far above the bottom edge the field already sits — usually the safe area inset. keyboardMode="dock" only.

Every part also accepts the underlying React Native props (ViewProps or TextProps) and a className for Tailwind utilities.

Notes

Accepts every TextInputProps except multiline and numberOfLines, both of which the component owns — it is always multiline, and its height comes from rows.

The height is computed rather than set as a class. A row count only becomes a height once you know how tall a line is, so the type size, its leading and the vertical padding are kept as numbers per size and the box is derived from them. Setting a line height in a class as well would give you two numbers that disagree.

autoGrow measures the text through onContentSizeChange and clamps the result between rows and maxRows. The field never shrinks below rows, so a field that has been typed into and cleared goes back to the height it started at rather than collapsing to one line.

showCount needs maxLength — a counter with no limit to count towards says nothing. It sits on the same row as the description, right-aligned, and turns destructive once the limit is reached.

errorMessage puts the field in its invalid state, the same as it does on Input: the border is tinted at rest as well as on focus, because the error is a fact about the value rather than about whether the field happens to be focused.

avoidKeyboard behaves exactly as it does on Input — it moves the field by its overlap with the keyboard and re-reads that overlap every frame while the field is focused. Do not toggle it at runtime; it changes which component renders the container, which would remount the field and drop focus.

On this page