Skip to content
v2.5.0
English
Русский
Documentation

TextArea

TextArea is the multiline text input for longer free-form content.
text-area.tsx · 6 lines
import { Label, TextArea } from '@vega-ui/react';

<div>
  <Label htmlFor='preview-issue'>What went wrong?</Label>
  <TextArea id='preview-issue' placeholder='Steps to reproduce, expected and actual result' rows={3} style={{ width: '100%' }} />
</div>
Browser support
Widely available
  • Chrome 111
  • Edge 111
  • Firefox 113
  • Safari 15.4
  • Safari on iOS 15.4
Bundle size
Tested in
13 tests

Summary

TextArea is the multiline text input for longer free-form content. It combines native textarea semantics with size, full-width, and error styling when the product value is intentionally multiline.

Imports

import { TextArea, type TextAreaProps, type TextAreaSize } from '@vega-ui/react';

Exported Types

  • TextAreaProps
  • TextAreaSize = 'sm' | 'md' | 'lg' | string

Basic Usage

issue-description.tsx · 22 lines
import { HelperText, Label, TextArea } from '@vega-ui/react';
import { useId } from 'react';

export default function IssueDescription() {
  const id = useId();
  const hintId = `${id}-hint`;

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%' }}>
      <Label htmlFor={id}>What went wrong?</Label>
      <TextArea
        id={id}

Variants

  • Size: sm, md, lg
  • State: default, error, disabled
  • Layout: regular width or fullWidth
  • Value model: controlled or uncontrolled

Edge Cases

  • Long content and resize behavior should be tested in constrained layouts.
  • fullWidth changes width, not height strategy.
  • TextArea is better for multiline content than forcing multi-line behavior into TextField.
  • Validation, helper text, and layout should account for long content, not just the first visible lines.

Common Mistakes

  • Using TextArea for short single-line values.
  • Forgetting helper text or labels around long-form input.
  • Treating error as a replacement for real validation messaging.

Examples

Basic

Basic: issue description with a hint

TextArea is the native <textarea> itself, so id, name and rows go straight on it. A Label points at it with htmlFor, and the hint is linked with aria-describedby.

issue-description.tsx · 22 lines
import { HelperText, Label, TextArea } from '@vega-ui/react';
import { useId } from 'react';

export default function IssueDescription() {
  const id = useId();
  const hintId = `${id}-hint`;

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%' }}>
      <Label htmlFor={id}>What went wrong?</Label>
      <TextArea
        id={id}

Controlled/Stateful

Controlled/Stateful: counter and live preview

The parent owns the value: the counter and the changelog preview below are derived from it on every keystroke. maxLength is a hard limit, so typing stops at 280 characters.

release-notes-counter.tsx · 39 lines
import { Label, Text, TextArea } from '@vega-ui/react';
import { useId, useState } from 'react';

const MAX_LENGTH = 280;

export default function ReleaseNotesCounter() {
  const id = useId();
  const counterId = `${id}-counter`;
  const [notes, setNotes] = useState('Faster search across archived invoices.\nDark mode for the billing portal.');
  const lines = notes.split('\n').filter((line) => line.trim() !== '');

  return (

Form/Integration

Form/Integration: support request with a send shortcut

required and minLength use native validation, and FormData reads the message on submit. Ctrl+Enter or ⌘+Enter calls requestSubmit(), so the shortcut runs the same validation as the button.

support-request-form.tsx · 45 lines
import { Button, HelperText, Label, Text, TextArea } from '@vega-ui/react';
import { FormEvent, KeyboardEvent, useId, useState } from 'react';

export default function SupportRequestForm() {
  const id = useId();
  const [sent, setSent] = useState('');

  const onSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const message = String(new FormData(event.currentTarget).get('message'));
    setSent(`Sent ${message.length} characters`);
    event.currentTarget.reset();

Layout/Overlay

Layout/Overlay: feedback form in a dialog

A fullWidth textarea fills the dialog, and autoFocus puts the caret in it on open. Enter adds a new line instead of submitting; Escape still closes the dialog.

feedback-in-dialog.tsx · 66 lines
import type { CSSProperties, FormEvent } from 'react';
import {
  Button,
  Dialog,
  DialogClose,
  DialogContent,
  DialogTrigger,
  Heading,
  HelperText,
  Icon,
  IconButton,
  Label,

Layout/Overlay: compact note composer

size='sm' with two rows keeps a secondary input small next to the content it annotates. The Label uses the matching size='sm'.

compact-internal-note.tsx · 50 lines
import { Button, Label, Text, TextArea } from '@vega-ui/react';
import { FormEvent, useId, useState } from 'react';

interface Note {
  id: number;
  text: string;
}

export default function CompactInternalNote() {
  const id = useId();
  const [notes, setNotes] = useState<Note[]>([
    { id: 1, text: 'Customer asked to move the renewal to March.' },

Error

Error: soft character limit

Unlike maxLength, a soft limit lets people paste a long text and then trim it. Past the limit the field gets error and aria-invalid, the hint switches to an error, and submit is blocked.

soft-limit-error.tsx · 40 lines
import { Button, HelperText, Label, TextArea } from '@vega-ui/react';
import { FormEvent, useId, useState } from 'react';

const LIMIT = 160;

export default function SoftLimitError() {
  const id = useId();
  const messageId = `${id}-message`;
  const [bio, setBio] = useState(
    'Product designer at Northwind. I work on billing, onboarding and everything in between, and I spend weekends restoring old film cameras and hiking the coastal trails near Porto.',
  );
  const over = bio.length - LIMIT;

Disabled

Disabled: locked while sending

A Fieldset with disabled disables the textarea and the button together while the request is in flight, so nothing can change or be sent twice. The form clears once the comment is posted.

disabled-while-sending.tsx · 37 lines
import { Button, Fieldset, Label, Text, TextArea } from '@vega-ui/react';
import { FormEvent, useId, useState } from 'react';

export default function DisabledWhileSending() {
  const id = useId();
  const [sending, setSending] = useState(false);
  const [comments, setComments] = useState<string[]>([]);

  const onSubmit = (event: FormEvent<HTMLFormElement>) => {
    event.preventDefault();
    const form = event.currentTarget;
    const comment = String(new FormData(form).get('comment')).trim();

Edge

Edge: grows with its content

field-sizing: content lets the textarea grow with its text, and maxHeight caps it, after which it scrolls. Browsers without field-sizing keep the height from rows.

auto-growing-notes.tsx · 24 lines
import type { CSSProperties } from 'react';
import { HelperText, Label, TextArea } from '@vega-ui/react';
import { useId } from 'react';

export default function AutoGrowingNotes() {
  const id = useId();

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%' }}>
      <Label htmlFor={id}>Rollout notes</Label>
      <TextArea
        id={id}

Anatomy

Overview

TextArea is a single-part primitive that renders the native <textarea> itself. Every prop the native element accepts goes straight to it, so there is no wrapper and no child part.

Required Parts

TextArea

Required. The native <textarea>: it owns the value, the size, the error and full-width styles, and takes name, rows, required, maxLength and the other native attributes.

Optional Parts

There are no exported child parts. A field is usually built from separate components around it:

Label

Optional, but needed in most UI. Names the textarea through htmlFor and its id.

HelperText

Optional. A hint or an error message, linked to the textarea with aria-describedby.

Composition Order

  1. Label
  2. TextArea
  3. optional HelperText or counter

Valid Composition Patterns

issue-description.tsx · 22 lines
import { HelperText, Label, TextArea } from '@vega-ui/react';
import { useId } from 'react';

export default function IssueDescription() {
  const id = useId();
  const hintId = `${id}-hint`;

  return (
    <div style={{ display: 'grid', gap: 'var(--spacing-3)', width: '100%' }}>
      <Label htmlFor={id}>What went wrong?</Label>
      <TextArea
        id={id}

The Label points at the textarea by id, and the hint below is linked with aria-describedby. The textarea is the form control itself, so name, rows and fullWidth go on it directly.

Invalid Composition Patterns

Children inside TextArea

A <textarea> takes its text from value or defaultValue. Children are not a supported way to set the content or to add icons and buttons inside the field.

Wrapping it in TextField

TextField expects TextFieldInput and is built for a single-line <input>. Use TextArea on its own for multiline text.

A placeholder instead of a label

The placeholder disappears as soon as the user types, so it cannot replace the Label.