MobTranslate styleguide

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

PartActual contract
SurfaceFull width, bounded min-width, readable 2px boundary, radius-md; content scrolls as needed
Sizessm: min-height 56px, 8px padding, text-sm; md: 80px, 12px, text-base; lg: 112px, 16px, text-lg
rows / CSS heightNative rows influences intrinsic size; minimum height and caller CSS still apply. Actual example uses rows=4.
Resizevertical default; none, horizontal and both alternatives. Horizontal resize remains bounded by max-width.
Help / counter / errorExternal text with IDs; native maxLength counts UTF-16 code units, not words or grapheme clusters.
Errorstate=error supplies default aria-invalid and semantic boundary; message remains caller-owned

Working multiline note

0 of 120 UTF-16 code units. This browser limit is not a count of spoken words or graphemes.

No local note submitted.

tsx
'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

PropContract
resize / size / statenone|vertical|horizontal|both; sm|md|lg; default|error. Defaults vertical/md/default.
value / defaultValue / onChangeNative textarea ownership and change event; do not trim every keystroke.
rows / cols / maxLength / minLength / requiredNative dimensions/constraints. Give clear task limits and validate the submitted value.
disabled / readOnly / nameNative enforcement and successful-control rules. Read-only can remain selectable and submitted.
className / style / ref / native ARIANative textarea props and HTMLTextAreaElement ref. Explicit naming/error connections.

States and behavior

StateRequired behavior
Empty / populatedKeep the visible label and instruction. Placeholder is an optional example, never the accessible name.
Hover / focusReadable boundary and token focus treatment. Focus should stay visible beneath persistent navigation.
ErrorName the problem and correction in text; connect it to the control and expose aria-invalid. A red border alone is insufficient.
Read-onlyFocusable/selectable value; native readOnly suppresses edits and retains a successful form control.
DisabledUnavailable interaction and exclusion from native FormData; preserve an explanation nearby.
PendingRetain the draft. Prevent duplicate requests and make the requested versus retained outcome explicit.
SuccessShow 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.