Textarea
StableA multi-line field that grows with what is typed, up to a maximum, then scrolls.
Uses CSS field-sizing where the browser supports it and measures its own height elsewhere. maxRows caps the growth so a long paste never pushes the page away. Takes every <textarea> attribute, and the ref reaches the textarea.
Updated View source
Grows as you type
import { useState } from 'react'import { Field, Textarea } from '@rchursin/product/ui'
const draft = `Sessions now pause when the laptop sleeps and pick up where they left off.Reminders snooze in 10, 30 or 60 minutes.The weekly email lists skills with no runs in 30 days.`
/** Type or paste a few lines: the field grows with them, up to eight lines, then scrolls. */export default function TextareaGrow() { const [notes, setNotes] = useState(draft)
return ( <div className="w-full max-w-md"> <Field label="Release notes" hint="One change per line. Shown on the changelog page."> {(p) => <Textarea {...p} name="notes" maxRows={8} value={notes} onChange={(e) => setNotes(e.target.value)} />} </Field> </div> )}Usage
import { Textarea } from '@rchursin/product/ui'<Field label="Release notes"> {(p) => <Textarea {...p} maxRows={8} value={notes} onChange={(e) => setNotes(e.target.value)} />}</Field>Examples
Comment box
Starts at one line and stops at five. Enter adds a line; ⌘ Enter (Ctrl Enter elsewhere) posts. Sample thread: nothing is sent.
import { useId, useState, type FormEvent, type KeyboardEvent } from 'react'import { Button, Textarea } from '@rchursin/product/ui'
/** * A comment box that starts at one line and stops growing at five. Enter adds a line; * ⌘ Enter (Ctrl Enter elsewhere) posts. Sample thread: nothing is sent. */export default function TextareaComposer() { const id = useId() const [text, setText] = useState('') const [comments, setComments] = useState(['Moved the weekly review to Friday so it lands after the cost report.'])
function post(e?: FormEvent) { e?.preventDefault() const body = text.trim() if (!body) return setComments((c) => [...c, body]) setText('') }
API
Textarea props
| Prop | Type | Default | Description |
|---|---|---|---|
minRows | number | 3 | The starting height in lines. It sets rows and a matching min-height (minRows × the line height, plus the padding), so the field starts that tall even where CSS field-sizing ignores rows. |
maxRows | number | 12 | Caps the height at that many lines (maxRows × the line height, plus the padding). Past it the field scrolls. |
value | string | number | readonly string[] | – | Controlled value (a string in practice). Where field-sizing is missing, the height is measured again each time value changes, so use it controlled for the growth to work everywhere. |
className | string | – | Extra classes on the <textarea>. |
style | CSSProperties | – | Merged over the min-height and max-height that minRows and maxRows set, so it can override either. |
Accessibility
Keyboard
| Key | What it does |
|---|---|
| Enter | Adds a new line. Bind a separate shortcut (⌘ Enter in the example) if Enter alone should not send. |
| Tab | Leaves the field. Tab never inserts a tab character. |
- It is a native <textarea>: labels, spell check, dictation and the browser’s undo all work.
- resize is off because the field sizes itself; past maxRows it scrolls with the keyboard and the pointer.
- Text is 16px on phones, so iOS never zooms in on focus.
Guidelines
Do
- Use it for answers longer than one line: notes, descriptions, messages.
- Pair it with CharacterCount when there is a limit.
- Keep it controlled so it grows in every browser.
Don’t
- Don’t send on Enter in a field meant for paragraphs.
- Don’t set a tiny maxRows for long answers: people need to see what they wrote.
In use
Related
- Character countA textarea that counts down to its limit, GOV.UK style: it never cuts text off, and speaks the count when typing pauses.
- FieldLabel above, hint under it, error in words between the hint and the control, with the ids and aria wiring done for you.
- Text inputA one-line field in the inset lux-input finish, in two sizes, with an optional icon inside and a slot on the right.
Source
The component’s own files, exactly as they run on this site.
/** * Form fields. Labels sit above inputs, hints under the label, errors between the hint and the * input with an icon and the word "Error:" for screen readers (never colour alone). * Optional fields say "(optional)"; required ones carry no mark. * * <Field label="Email" hint="We send the sign-in code here." error={err}> * {(p) => <TextInput {...p} type="email" autoComplete="email" />} * </Field> */import { forwardRef, useId, useLayoutEffect, useRef, useState, type InputHTMLAttributes, type KeyboardEvent, type ReactNode, type SelectHTMLAttributes, type TextareaHTMLAttributes,