Textarea
textareaCollects multiline text in an auto-sizing field.
Usage
Basic usage
For multi-line input, rows controls the initial number of visible lines (default 3).
<Textarea placeholder="Write something..." className="w-64" />Size
size provides three levels of sm / md (default) / lg.
<>
<Textarea size="sm" placeholder="sm" className="w-64" />
<Textarea size="md" placeholder="md" className="w-64" />
<Textarea size="lg" placeholder="lg" className="w-64" />
</>Adaptive height
autoResize automatically grows in height according to the content, and rows is used as the lower limit.
<Textarea autoResize defaultValue={"Grow taller with the content\n second line\n third line"} className="w-64" />Inline cell editing
variant="cell" strips the shell and hands height to CSS field-sizing: content (the rows lower bound already defaults to 1, so no per-cell value is needed). Focus shows a tinted background plus an inset underline instead of a ring, so nothing spills into the neighbouring cell.
<Textarea variant="cell" defaultValue="Edit in place; a new line grows the box" aria-label="Note" />Invalid state
invalid marked with red border and focus ring (manual transmission when used independently).
<Textarea invalid defaultValue="Wrong content" className="w-64" />Disabled
disabled Reduce transparency and block interaction.
<Textarea disabled defaultValue="Disabled" className="w-64" />When to use
Use Textarea for multiline notes, descriptions, or messages. Use Input for one line, or Select for a fixed option. Enable autoResize to grow with content while treating rows as the minimum height. Inside HulianUI Field, label, error, and ARIA associations are inherited automatically.
Import
import { Textarea, textareaVariants } from "@hulianui/ui"Props
Inherit the native <textarea> properties (except size is overwritten, such as value/onChange/rows/placeholder/disabled…).
| Name | Type | Default | Description |
|---|---|---|---|
| size | "xs" | "sm" | "md" | "lg" | "md" | Size (CVA variant, overrides native size). xs matches the xs height of Input and SelectTrigger for dense tables. Under variant="cell" it only affects font size; there is no padding left to change |
| ref | Ref<HTMLTextAreaElement> | - | Forwarded to the inner native <textarea>. focus(), select(), reading .value, and react-hook-form register() all rely on it, and it coexists with the internal ref used by autoResize |
| variant | "default" | "cell" | "default" | Shell form. cell is the in-place editor for a table cell: no border, transparent background, zero padding, height follows content through CSS field-sizing: content, and focus is shown as a tinted background plus an inset underline instead of a focus ring |
| value | string | number | readonly string[] | null | - | Controlled value. Beyond the native types it also accepts `null` and renders it as an empty string (#220, same as Input): register().value from `useForm` passes an explicitly cleared null straight through. Omitting it (undefined) still means uncontrolled |
| invalid | boolean | false | Marked red when used independently; automatically driven by Field.Root invalid in hulian Field |
| autoResize | boolean | false | Adapt height according to content (JS scrollHeight, rows is the lower limit) |
| rows | number | 3 (1 under variant="cell") | Initial/minimum row height |
| disabled | boolean | false | Disable |
Events
| Event | Type | Description |
|---|---|---|
| onChange | (e: ChangeEvent<HTMLTextAreaElement>) => void | Transparently transmit native input callback (used with value when controlled) |
Example
<Textarea placeholder="Write something…" className="w-64" />{/* Adaptive height */}
<Textarea autoResize defaultValue={"Grows with content\nSecond line\nThird line"} className="w-64" />{/* Inline editing in a table: the cell itself is a self-growing multi-line input, no className needed */}
<Textarea variant="cell" value={value} onChange={(e) => setValue(e.target.value)} aria-label="Note" />Usage guidelines
variant="cell"andautoResizesolve the same problem two ways: the former hands height to CSSfield-sizing: content(native, no JavaScript round trip), the latter measuresscrollHeightin JavaScript. `cell` alone is the default choice.field-sizingis a recent CSS feature; browsers without it fall back to the fixed height implied byrows. Nothing breaks, the box simply stops growing. PassautoResizealongside when you need to cover those browsers: it writes an inlinestyle.height, which outranks the intrinsic size fromfield-sizing, so the two never fight.- Use
variant="cell"for in-place editing instead of writingclassName="border-0 bg-transparent p-0 resize-none field-sizing-content …"at the call site. Therowslower bound already defaults to1undercell, so there is no need to passrows={1}per cell either. variant="cell"andautoResizedo not accept aresize-*override, and the override only half lands: this variant emitsresize-none overflow-hidden, and the two sit in different tailwind-merge groups, soclassName="resize-y"replacesresize-nonewhileoverflow-hiddenstays untouched. The handle drags, but dragging smaller clips the content with no scrollbar (andfield-sizing-contentkeeps sizing to content, fighting the inline height the handle writes). The component names the misuse throughconsole.warnin development, once per variant. For a box the user can drag, switch tovariant="default"and do not passautoResize.- See [[base-ui-field-control-render-textarea-type-safe]] when extending a Field-aware Textarea. Base UI Field has no Textarea primitive, so textarea-specific props such as
ref,rows, andonInputbelong on therenderelement rather thanField.Control; otherwise TypeScript rejects the ref or Field accessibility wiring is silently lost. - Do not repeat
invalidinside HulianUI Field; Field.Root drives it automatically.
Related
Playground
<Textarea size="md" placeholder="Write something…" rows={3} />