Textarea
Multi-line text field that grows from minRows to maxRows, with a character counter when maxLength is set.
Componentforms
Style
Light
Dark
Your app is ready.
Inter body text, Inter headings.
smmdlgxl
you@example.com
Continue
a0: Vega, neutral, neutral accent, default radius, Inter.Change PresetInstallation
$
npx nativecn-cli@latest add textareaAlso installs: theme, input, text, use-controllable-state.
Usage
Textarea demo
A Textarea with a character counter inside a FormField.
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>
);
}Textarea demo
A Textarea with a character counter inside a FormField.
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,lgstatus: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
The Vega Style's code, exactly as add copies it into your app.
{components}/textarea.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>
);
}{components}/textarea.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>
);
}
