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
- Oklab and OkLCh — comes from the design tokens
- :where()
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
TextAreaPropsTextAreaSize = 'sm' | 'md' | 'lg' | string
Basic Usage
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.
fullWidthchanges width, not height strategy.TextAreais better for multiline content than forcing multi-line behavior intoTextField.- Validation, helper text, and layout should account for long content, not just the first visible lines.
Common Mistakes
- Using
TextAreafor short single-line values. - Forgetting helper text or labels around long-form input.
- Treating
erroras 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.
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.
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.
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.
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'.
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.
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.
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.
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
LabelTextArea- optional
HelperTextor counter
Valid Composition Patterns
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.