Textarea
When to use
Section titled “When to use”- Comments, messages and descriptions
- Any free text that may run over several lines
Not for a single line — use an Input.
Choosing between entering text:
| Component | Use it for | For example |
|---|---|---|
| Input | One line of text: a name, an email, a search | Email address, search box |
| OTP input | A code of fixed length, typed or pasted one character per box | SMS code, two-factor code, PIN |
| Textarea | Several lines of free text that may wrap | Comment, description, message |
| Combobox | Typing to find and pick from a list of known options | Choose a city, add existing tags |
| Field | Not a control: wraps any of these to tie a label, a hint and an error to it | Any form control with validation |
Textarea is a real <textarea>. Typing, selection, spellcheck, validation and form submission
are all the platform’s, so there is no headless half and nothing to keep in sync. It works with
react-hook-form’s register() as-is.
import { Textarea } from '@arun-dev/ui';
// A definite column, so the textarea's width cap resolves: an auto track would grow with// whatever width the user drags to.const field = { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr)', gap: 'var(--space-3xs)', maxInlineSize: '24rem',};
export default function TextareaBasics() { return ( <> <div style={field}> <label htmlFor="textarea-bio">Bio</label> <Textarea id="textarea-bio" rows={4} placeholder="A few lines about yourself" /> </div>
<div style={field}> <label htmlFor="textarea-reason">Reason for leaving</label> <Textarea id="textarea-reason" aria-invalid aria-describedby="textarea-reason-error" /> <small id="textarea-reason-error" style={{ color: 'var(--color-status-error)' }}> Tell us why, so we can improve. </small> </div>
<div style={field}> <label htmlFor="textarea-disabled">Disabled</label> <Textarea id="textarea-disabled" defaultValue="Read me, don't edit me" disabled /> </div> </> );}Playground
Section titled “Playground”<Textarea placeholder="Write something…" />One element
Section titled “One element”Unlike Input, there is no box and there are no slots. The <textarea>
draws its own frame, so every prop lands on it, className included. A <label htmlFor>,
aria-describedby, register() and a focus ref all reach the control directly.
Resizing
Section titled “Resizing”The field fills its container’s width by default, and the user can drag the corner handle to
change both height and width. Width can shrink to --textarea-min-width and grow back to the
container, but never past it, so a dragged field cannot break the layout around it. Set the
starting width with className or style; the container stays the ceiling.
In a grid, give the column a size
The ceiling is max-inline-size: 100%, which holds in normal flow, in flex layouts and in grid
columns with a definite size. An auto grid column instead grows to fit its content, including
a width the user dragged, so the field can push past the grid. Size the column, e.g.
grid-template-columns: minmax(0, 1fr).
To lock a dimension, override resize in your own CSS, e.g. resize: vertical.
Auto-resize
Section titled “Auto-resize”autoResize makes the field grow with what is typed, from --textarea-min-height up to
--textarea-max-height, then scroll. Height is no longer the user’s to set, so the handle
drags width only.
import { Textarea } from '@arun-dev/ui';
// A definite column, so the textarea's width cap resolves: an auto track would grow with// whatever width the user drags to.const field = { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr)', gap: 'var(--space-3xs)', maxInlineSize: '24rem',};
export default function TextareaAutoResize() { return ( <div style={field}> <label htmlFor="textarea-message">Message</label> {/* Type or paste a few paragraphs: it grows to --textarea-max-height, then scrolls. */} <Textarea id="textarea-message" autoResize placeholder="Type a few lines…" /> </div> );}CSS only
Auto-resize is field-sizing: content, with no JavaScript measuring the text. Where a browser
lacks it, the textarea keeps its rows height and scrolls, just as it would without the prop.
Controlled
Section titled “Controlled”Uncontrolled is the default: pass defaultValue, or register(), and the value lives on the
<textarea>. When something else needs the value as it is typed, such as a counter, a preview or
a reset button, control it with value and onChange, exactly as with a plain <textarea>.
React’s onChange fires on every keystroke, not on blur.
import { useState } from 'react';import { Button, Textarea } from '@arun-dev/ui';
// A definite column, so the textarea's width cap resolves: an auto track would grow with// whatever width the user drags to.const field = { display: 'grid', gridTemplateColumns: 'minmax(0, 1fr)', gap: 'var(--space-3xs)', maxInlineSize: '24rem',};
const LIMIT = 140;
export default function TextareaControlled() { const [status, setStatus] = useState('Shipping the Textarea docs'); const over = status.length > LIMIT;
return ( <div style={field}> <label htmlFor="textarea-status">Status</label> {/* React state owns the value: every keystroke goes through onChange and back. */} <Textarea id="textarea-status" autoResize value={status} onChange={(e) => setStatus(e.target.value)} aria-invalid={over || undefined} aria-describedby="textarea-status-count" /> <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> <small id="textarea-status-count" aria-live="polite" style={{ color: over ? 'var(--color-status-error)' : 'var(--color-text-muted)' }} > {status.length} / {LIMIT} </small> <Button onClick={() => setStatus('')}>Clear</Button> </div> </div> );}autoResize keeps working, since it sizes to whatever value the element holds. Pass the value as
value, not as children.
States
Section titled “States”All of them come from the native control. There is no data-* attribute to set:
| State | Set it with |
|---|---|
| focus | the platform (:focus-visible) |
| invalid | aria-invalid on the textarea |
| disabled | disabled on the textarea |
Point aria-describedby at the error text too, so it is read out with the field.
| Prop | Type | Default | Description |
|---|---|---|---|
autoResize | boolean | false | Grow with the content between --textarea-min-height and --textarea-max-height, then scroll. CSS only (field-sizing: content); browsers without it keep the rows height. |
className | string | Concatenated with the component's own classes, never replacing them. | |
render | ReactElement | Element or component to render instead of the default. Props, className, event handlers and ref are merged onto it. | |
ref | Ref<HTMLElement> | Ref to the rendered element. Merged with any ref on the render element. | |
…rest | HTMLAttributes | Anything else is spread onto the rendered element, so id, aria-*, data-* and event handlers all reach the DOM. |
- Set the starting height with
rows, and cap input withmaxLength. Both are native. - Restyle every textarea with the
--textarea-*tokens, e.g.--textarea-min-heightor--textarea-border-color-focus. They match Input’s by default but are independent: changing--input-*does not move textareas.