React Textarea

Textareas in React are controlled the same way text inputs are, but with one HTML quirk you need to unlearn: the value lives in the value attribute, not as inner text.

The HTML Quirk React Fixes

In plain HTML, a <textarea> stores its content as a child text node: <textarea>Hello</textarea>. React normalizes this so that a <textarea> behaves like every other form field — its content is set through a value attribute, exactly like <input value="...">. This consistency is what makes controlled textareas simple to reason about.

Building a Controlled Textarea

A controlled textarea reads its displayed text from a piece of state and writes back to that state on every onChange event. Because React re-renders with the new state, the textarea's value is always in sync with your component's data — there's no gap where the DOM and your JavaScript disagree about the current text.

Controlled Textarea with useState

import { useState } from 'react';

function FeedbackForm() {
  const [message, setMessage] = useState('');

  return (
    <form>
      <label htmlFor="feedback">Your feedback</label>
      <textarea
        id="feedback"
        value={message}
        onChange={(e) => setMessage(e.target.value)}
        rows={5}
        placeholder="Tell us what you think..."
      />
      <p>{message.length} characters</p>
    </form>
  );
}

export default FeedbackForm;
Note: Never write <textarea>{message}</textarea> in React. It still works in some versions but it's deprecated behavior and mixing it with a value prop causes a 'contains both value and children' warning.

Sizing and Resizing

The rows and cols attributes set the initial visible size in character units, while CSS resize (none, vertical, horizontal, both) controls whether the user can drag to resize it. For a textarea that grows with content, you generally either set rows dynamically based on line count or reach for a small auto-resize library — React itself has no built-in autosize behavior.

  • rows: number of visible text lines (default 2)
  • cols: approximate visible width in characters (rarely used; prefer CSS width)
  • maxLength: caps how many characters the user can type
  • readOnly: still submits with the form but the user can't edit it
  • disabled: excluded from form submission and cannot be focused

Validating and Limiting Input

Because onChange gives you the full new string on every keystroke, validation is just a conditional inside the handler. You can reject changes that would exceed a limit, strip disallowed characters, or simply mark the field invalid for a submit-time check.

Enforcing a Soft Character Limit with a Warning

import { useState } from 'react';

function BioField() {
  const MAX = 160;
  const [bio, setBio] = useState('');
  const isOverLimit = bio.length > MAX;

  return (
    <div>
      <textarea
        value={bio}
        onChange={(e) => setBio(e.target.value)}
        rows={4}
      />
      <p style={{ color: isOverLimit ? 'crimson' : 'gray' }}>
        {bio.length}/{MAX}
      </p>
    </div>
  );
}

export default BioField;
PropPurpose
valueCurrent text, driven by state (controlled)
onChangeFires on every keystroke with the updated string
rows / colsInitial visible dimensions
placeholderGrey hint text shown when empty
Note: If you truly don't need React to track keystrokes, an uncontrolled textarea with defaultValue and a ref (textareaRef.current.value) is a valid lighter-weight alternative for things like a one-off form you only read on submit.

Exercise: React Textarea

In plain HTML, a textarea's content sits between its opening and closing tags. How does React handle this?