# Textarea

> Multi-line text field that grows from minRows to maxRows, with a character counter when maxLength is set.

Component · forms. Registry item `textarea`, shown in the vega Style. JSON: https://nativecn.dev/r/styles/vega/textarea.json

## Installation

```sh
npx nativecn-cli@latest add textarea
```

Also installs: [theme](https://nativecn.dev/docs/theme/theme), [input](https://nativecn.dev/docs/components/input), [text](https://nativecn.dev/docs/components/text), [use-controllable-state](https://nativecn.dev/docs/primitives/use-controllable-state).

## Usage

### Textarea demo

A Textarea with a character counter inside a FormField.

```tsx
import { FormField } from "@/components/form-field";
import { Textarea } from "@/components/textarea";

export function TextareaDemo() {
  return (
    <FormField label="Bio" description="Tell people a little about yourself.">
      <Textarea placeholder="I like…" maxLength={160} minRows={3} maxRows={6} />
    </FormField>
  );
}
```

## Props

| Prop | Type and description |
|---|---|
| `...InputProps` | Input's props except icon and secureTextEntry |
| `minRows` | number: rows shown when empty (default 3) |
| `maxRows` | number: grows up to this many rows, then scrolls (default 8) |
| `maxLength` | number: limits the text and shows an n/max counter |
| `style` | ViewStyle, layout only, merged last onto the root |

## Variants

- `size`: `sm`, `md`, `lg`
- `status`: `error`, `success`

## Accessibility

- Named and hinted like Input: aria-label or the FormField label is the name, and the error or description the hint.
- A new status is announced once, unless a FormField is already announcing its error.
- disabled sets aria-disabled and makes the field read-only.
- The counter is read as "n of max characters".

## Notes

FocusChain skips Textarea so Enter keeps adding new lines. Works controlled (value + onChangeText) or uncontrolled (defaultValue).

## Source

### {components}/textarea.tsx

```tsx
import { StyleSheet, TextInput, View } from "react-native";

import { useInputStyles, useTextField, type InputProps } from "@/components/input";
import { Text } from "@/components/text";
import { useControllableState } from "@/hooks/use-controllable-state";
import { createStyles, useTheme } from "@/theme";

export type TextareaProps = Omit<InputProps, "icon" | "multiline" | "secureTextEntry"> & {
  /** Visible rows when empty (default `3`). */
  minRows?: number;
  /** The field grows with its text up to this many rows, then scrolls (default `8`). */
  maxRows?: number;
};

const useStyles = createStyles((t) => ({
  counter: { marginTop: t.spacing[1] },
}));

/**
 * A multi-line text field that grows with its text from `minRows` to `maxRows`. Setting
 * `maxLength` adds a character counter. Same frame, Colour Roles and FormField wiring as Input;
 * FocusChain skips it, so Enter keeps adding new lines. Works controlled (`value` +
 * `onChangeText`) or uncontrolled (`defaultValue`). `style` is merged last onto the root.
 */
export function Textarea({
  size = "md",
  minRows = 3,
  maxRows = 8,
  maxLength,
  value,
  defaultValue,
  onChangeText,
  status,
  disabled,
  editable,
  placeholder,
  placeholderTextColor,
  "aria-label": ariaLabel,
  accessibilityHint,
  onFocus,
  onBlur,
  style,
  ref,
  ...props
}: TextareaProps) {
  const { colors } = useTheme();
  const styles = useInputStyles();
  const own = useStyles();
  const f = useTextField({
    disabled,
    status,
    placeholder,
    "aria-label": ariaLabel,
    accessibilityHint,
    onFocus,
    onBlur,
  });
  const [text, setText] = useControllableState({
    value,
    defaultValue: defaultValue ?? "",
    onChange: onChangeText,
  });

  const { lineHeight = 0, paddingVertical = 0 } = StyleSheet.flatten(styles.multilineText) as {
    lineHeight?: number;
    paddingVertical?: number;
  };
  const rows = (n: number) => n * lineHeight + 2 * paddingVertical;
  const minHeight = rows(minRows);
  const maxHeight = rows(Math.max(minRows, maxRows));

  return (
    <View style={style}>
      <View
        style={[
          styles.root,
          styles[size],
          styles.autoHeight,
          f.focused ? styles.focused : null,
          f.status ? styles[f.status] : null,
          f.disabled ? styles.disabled : null,
        ]}
      >
        <TextInput
          ref={ref}
          multiline
          editable={!f.disabled && editable !== false}
          placeholder={placeholder}
          placeholderTextColor={placeholderTextColor ?? colors.mutedForeground}
          selectionColor={colors.primary}
          maxLength={maxLength}
          value={text}
          onChangeText={setText}
          style={[styles.text, styles.multilineText, { minHeight, maxHeight }]}
          {...props}
          {...f.inputProps}
        />
      </View>
      {maxLength != null ? (
        <Text
          variant="caption"
          color="mutedForeground"
          align="right"
          style={own.counter}
          aria-label={`${text.length} of ${maxLength} characters`}
        >
          {`${text.length}/${maxLength}`}
        </Text>
      ) : null}
    </View>
  );
}
```
