Textarea
Native multiline text with a persistent label, honest length instructions and accessible editing space.
Choose multiline input
Use Textarea for notes, descriptions or paragraphs. Enter inserts a newline; ordinary multiline input should not unexpectedly submit. Use Input for short single-line values and a specialized editor only when rich formatting is a real requirement.
Textarea renders a native textarea and does not register itself with Field. Use explicit htmlFor/id and aria-describedby/aria-invalid relationships, or an external form integration that actually validates and connects this native element. Do not claim automatic Field validation from visual nesting.
Anatomy and specifications
| Part | Actual contract |
|---|---|
| Surface | Full width, bounded min-width, readable 2px boundary, radius-md; content scrolls as needed |
| Sizes | sm: min-height 56px, 8px padding, text-sm; md: 80px, 12px, text-base; lg: 112px, 16px, text-lg |
| rows / CSS height | Native rows influences intrinsic size; minimum height and caller CSS still apply. Actual example uses rows=4. |
| Resize | vertical default; none, horizontal and both alternatives. Horizontal resize remains bounded by max-width. |
| Help / counter / error | External text with IDs; native maxLength counts UTF-16 code units, not words or grapheme clusters. |
| Error | state=error supplies default aria-invalid and semantic boundary; message remains caller-owned |
Working multiline note
'use client';
import { useRef, useState } from 'react';
import { Button, Input, Textarea, type InputProps } from '@mobtranslate/ui';
export function NativeInputExample() {
const [value, setValue] = useState('');
const [variant, setVariant] = useState<NonNullable<InputProps['variant']>>('outline');
const [size, setSize] = useState<NonNullable<InputProps['size']>>('md');
const [readOnly, setReadOnly] = useState(false);
const [disabled, setDisabled] = useState(false);
const [error, setError] = useState('');
const [result, setResult] = useState('No local input submitted.');
const input = useRef<HTMLInputElement>(null);
return <form data-native-input-lab noValidate className="space-y-4" onSubmit={event => {
event.preventDefault();
if (!disabled && !value.trim()) { setError('Enter a local display label.'); input.current?.focus(); return; }
setError(''); setResult(`Local FormData: ${new FormData(event.currentTarget).get('displayLabel') ?? 'displayLabel omitted (disabled)'}. No data was sent.`);
}}>
<div className="flex flex-wrap gap-3"><label>Appearance <select data-input-variant className="mt-input" value={variant} onChange={event => setVariant(event.target.value as typeof variant)}><option>outline</option><option>filled</option></select></label><label>Density <select data-input-size className="mt-input" value={size} onChange={event => setSize(event.target.value as typeof size)}><option>sm</option><option>md</option><option>lg</option></select></label></div>
<label htmlFor="native-display-label">Local display label (required when available)</label>
<Input ref={input} id="native-display-label" name="displayLabel" value={value} onChange={event => { setValue(event.target.value); setError(''); }} variant={variant} size={size} readOnly={readOnly} disabled={disabled} required autoComplete="off" maxLength={80} aria-describedby={error ? 'native-input-help native-input-error' : 'native-input-help'} state={error ? 'error' : 'default'} />
<p id="native-input-help">English-only local example. A visible label remains when the value is filled.</p>
{error && <p id="native-input-error" role="alert">{error}</p>}
<div className="flex flex-wrap gap-3"><Button type="button" variant="outline" onClick={() => setReadOnly(current => !current)}>{readOnly ? 'Allow local input editing' : 'Make local input read-only'}</Button><Button type="button" variant="outline" onClick={() => setDisabled(current => !current)}>{disabled ? 'Enable local input' : 'Disable local input'}</Button><Button type="submit">Submit native input</Button></div>
<p role="status">{result}</p>
</form>;
}
export function NativeTextareaExample() {
const [value, setValue] = useState('');
const [readOnly, setReadOnly] = useState(false);
const [resize, setResize] = useState<'none' | 'vertical' | 'horizontal' | 'both'>('vertical');
const [error, setError] = useState('');
const [result, setResult] = useState('No local note submitted.');
const textarea = useRef<HTMLTextAreaElement>(null);
return <form data-textarea-lab noValidate className="space-y-4" onSubmit={event => {
event.preventDefault();
if (!value.trim()) { setError('Write a local example note before submitting.'); textarea.current?.focus(); return; }
setError(''); setResult(`Local note retained: ${String(new FormData(event.currentTarget).get('note'))}. No data was sent.`);
}}>
<label htmlFor="native-note">Local example note (required)</label>
<Textarea ref={textarea} id="native-note" name="note" value={value} onChange={event => { setValue(event.target.value); setError(''); }} rows={4} maxLength={120} required readOnly={readOnly} resize={resize} state={error ? 'error' : 'default'} aria-describedby={error ? 'native-note-help native-note-error' : 'native-note-help'} />
<p id="native-note-help">{value.length} of 120 UTF-16 code units. This browser limit is not a count of spoken words or graphemes.</p>
{error && <p id="native-note-error" role="alert">{error}</p>}
<label>Resize behavior <select data-textarea-resize className="mt-input" value={resize} onChange={event => setResize(event.target.value as typeof resize)}>{['none','vertical','horizontal','both'].map(item => <option key={item}>{item}</option>)}</select></label>
<div className="flex flex-wrap gap-3"><Button type="button" variant="outline" onClick={() => setReadOnly(current => !current)}>{readOnly ? 'Allow local note editing' : 'Make local note read-only'}</Button><Button type="submit">Submit native note</Button></div>
<p role="status">{result}</p>
</form>;
}
API reference
| Prop | Contract |
|---|---|
| resize / size / state | none|vertical|horizontal|both; sm|md|lg; default|error. Defaults vertical/md/default. |
| value / defaultValue / onChange | Native textarea ownership and change event; do not trim every keystroke. |
| rows / cols / maxLength / minLength / required | Native dimensions/constraints. Give clear task limits and validate the submitted value. |
| disabled / readOnly / name | Native enforcement and successful-control rules. Read-only can remain selectable and submitted. |
| className / style / ref / native ARIA | Native textarea props and HTMLTextAreaElement ref. Explicit naming/error connections. |
States and behavior
| State | Required behavior |
|---|---|
| Empty / populated | Keep the visible label and instruction. Placeholder is an optional example, never the accessible name. |
| Hover / focus | Readable boundary and token focus treatment. Focus should stay visible beneath persistent navigation. |
| Error | Name the problem and correction in text; connect it to the control and expose aria-invalid. A red border alone is insufficient. |
| Read-only | Focusable/selectable value; native readOnly suppresses edits and retains a successful form control. |
| Disabled | Unavailable interaction and exclusion from native FormData; preserve an explanation nearby. |
| Pending | Retain the draft. Prevent duplicate requests and make the requested versus retained outcome explicit. |
| Success | Show what was retained and where. A success appearance alone proves no save. |
Editing and accessibility
- Tab navigates controls; Enter creates a newline; selection, paste and composition retain native behavior. A deliberate send shortcut must be explained and must not remove an ordinary submit control.
- Keep enough visible rows to read and correct the expected content. A resize handle is useful but is not the only way to access a long value; preserve scrolling.
- Announce submission errors and focus the textarea when it needs correction. A rapidly changing character counter usually does not need an assertive live region.
- For user-authored content use the actual language metadata and direction where known; interface localization does not authorize invented translations.
Failures and recovery
- Do not disable paste, destroy line breaks, replace the label with a counter, or make every keystroke an error announcement.
- Length models differ across scripts. State the actual enforced unit; use a grapheme-aware policy only with matching client and server rules.
- After failure retain text. Distinguish an unsaved draft, a locally retained note and a confirmed remote revision. Unmounting this local example clears its state.
Verification and release scope
- Verify the actual label/control connection, description/error IDs, invalid state, focus order and corrected input with pointer and keyboard. Read the submitted payload rather than inferring it from visible text.
- Test blank, populated, invalid, disabled, read-only and pending/recovery states. Check both themes, long labels, user text spacing, 320px reflow, reduced motion, forced colors and zoom.
- Run the named input-family browser checks and source/type/build checks. Inspect mobile keyboards, autofill, password managers and a named screen-reader/browser pair separately. DOM tests do not prove their behavior.
- Client validation supports correction; it does not establish authorization, consent, source authority, language quality, identity, security or a successful server write.