A labeled multi-line text field on the Field contract, a native <textarea>: pass label, and error from your server action or client check. autoGrow makes it grow with the text from rows lines up to maxRows.
On this site
This site's own pages use TextArea too. Open one with X-ray pointing it out: The Nautilus's story
In Spoonjoy
Spoonjoy, a recipe app, is built with Ouro HX (how).
In its source: app/components/recipe/IngredientParseInput.tsx · app/components/moderation/ReportOrBlockButton.tsx · app/components/recipe/RecipeBuilder.tsx
Install
Once per app, with Tailwind CSS v4 and an @/* path alias (Ouro HX is built on Base UI, so --base base; skip -t in an existing app):
npx shadcn@latest init --base base --preset nova --yes --no-monoreponpx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.3.10Then the component. From v0.8.0 it installs only the helpers it imports and never touches your stylesheet (Installing):
npx shadcn@latest add ourostack/ouro-ui/text-area#v1.3.10Or, with the @ouro namespace set up:
npx shadcn@latest add @ouro/ouro-base @ouro/text-areaIts tests, stories and Playwright spec are optional. They need Vitest 4.1.11 or later, and from v0.8.0 never change a version your app has (Testing):
npx shadcn@latest add ourostack/ouro-ui/text-area#v1.3.10 ourostack/ouro-ui/text-area-tests#v1.3.10import { TextArea } from "@/components/ouro/text-field/text-area";Paths use shadcn's default @/ alias; the CLI rewrites them to yours.
Example
<TextArea label="Notes" name="notes" description="Only you see these." />
<TextArea label="Message" name="message" autoGrow rows={2} maxRows={8} required error={errors?.message} />
// Words an assistant suggested, marked in the person's draft; the marks follow edits.
<TextArea
label="Reply"
value={text}
onValueChange={setText}
highlights={[{ start: 0, end: 22, label: "Suggested by your assistant" }]}
onHighlightsChange={setHighlights}
/>
// A chat composer: the box is the app's, and the textarea inside it has no frame.
<form className="flex items-end gap-2 rounded-xl border border-border p-2 focus-within:border-ring">
<TextArea label="Message" hideLabel name="message" placeholder="Ask anything" variant="plain" autoGrow rows={1} maxRows={8} className="flex-1" />
<Button type="submit">Send</Button>
</form>Usage
variant="plain" draws no frame, for a textarea inside a box your app draws, and size="lg" sets 20px text. highlights marks ranges of the text in place, each with a tone and a label screen readers hear, and the marks follow their words as the person edits.
Use instead when
- Use
TextFieldinstead for one line,Composerfor a chat message box with its send button, andFieldwithFieldControlfor a control that is not text.
Props
Type: TextAreaProps.
autoGrow- Type:
boolean - Grows with the content, from
rowslines up tomaxRows, instead of scrolling. Use it for messages and notes, so people see what they wrote. className- Type:
string - Layout only: margins, width, grid placement (
mt-4,col-span-2). Appearance classes warn in development; style fields in your theme. defaultValue- Type:
string - The starting text, for an uncontrolled field.
description- Type:
ReactNode - Supporting text under the label, read after the label through
aria-describedby. disabled- Type:
boolean - Disables the field and fades it.
error- Type:
ReadonlyErrorMessages - What is wrong, from your server action or client check. The message (or several, as a list) shows under the field, which gets
aria-invalid. A message is text or a React node (one with a link); an empty string or array means valid. hideLabel- Type:
boolean - Hides the label visually; screen readers still read it as the field's name.
highlights- Type:
readonly TextAreaHighlight[] - Ranges of the text to mark in place, each with a tone and a label: words an assistant wrote in the person's draft, or spelling and grammar notes. The marks wrap with the words and scroll with them, and screen readers hear each label with the words it marks in the field's description. As the person types, pastes or undoes, the marks follow their words: what the person types is never marked, an edit inside a mark splits it, and undo brings back the marks the text had. Pass
[]for none, rather than switching the prop off and on, which remounts the textarea: with no marks the layer draws nothing and costs nothing as the person types. label(required)- Type:
string - What the field is for, shown above it and read as its name. Required: every control needs a name. To hide it visually, keep it and add
hideLabel. A placeholder is not a label. maxRows- Type:
number - With
autoGrow, the most lines it grows to before it scrolls. Unset, it keeps growing. name- Type:
string - The form field name the text submits under.
onHighlightsChange- Type:
((highlights: TextAreaHighlight[]) => void) - Called with the highlights moved to fit the text after the person edits it. Store them and pass them back as
highlights, or recompute your own from your data. onValueChange- Type:
FieldControlProps["onValueChange"] - Called with the new text on every change.
placeholder- Type:
string - An example of what to write, shown while the field is empty. It is not a label; pass
label, which stays visible. ref- Type:
Ref<HTMLTextAreaElement> - The textarea element.
required- Type:
boolean - The field must be filled: it gets
requiredandaria-required, and the label a marker. rows- Type:
number - Default:
3 - Lines of text it shows; with
autoGrow, the fewest it shows. size- Type:
"md" | "lg" - Default:
"md" - Text size.
mdis 16px on phones and steps down to 14px from thesmbreakpoint (640px).lgis 20px at every width. The font family is inherited, so set a display font on a parent. value- Type:
string - The current text, for a controlled field; pair it with
onValueChangeoronChange. variant- Type:
"outline" | "plain" - Default:
"outline" - The frame.
outlinedraws the bordered box.plaindraws no border, background or padding, for a textarea inside a frame your app draws (a chat composer with its send button in the same box); it keeps the focus outline, and the error message still shows under it, but with no border to turn red, color your own frame for an invalid field. Any other string is a type error whose message names the fix.
Native attributes
Also accepts the attributes in React's HTMLAttributes and TextareaHTMLAttributes (id, aria-*, event handlers), passed to the rendered element, except those named on this page.
Don't use
These are not props. Passing one fails the type check with the fix in the error, and in development a console message names the fix.
autoResize: Not a prop. Ouro UI usesautoGrow:<TextArea autoGrow>.autosize: Not a prop. Ouro UI usesautoGrow:<TextArea autoGrow>.errorMessage: Not a prop. Ouro UI useserror:<TextArea error="Required.">.helperText: Not a prop. Ouro UI usesdescription:<TextArea description="Only you see these.">.invalid: Not a prop.errormarks the field invalid:<TextArea error="What is wrong">.isDisabled: Not a prop. Ouro UI usesdisabled:<TextArea disabled>.isInvalid: Not a prop.errormarks the field invalid:<TextArea error="What is wrong">.isRequired: Not a prop. Ouro UI usesrequired:<TextArea required>.marks: Not a prop. Ouro UI useshighlights:<TextArea highlights={[{ start: 0, end: 5, label: "Suggested" }]}>.minRows: Not a prop.rowsis the minimum:<TextArea autoGrow rows={2} maxRows={8}>.
className is for layout only
Pass margins, width and grid placement (mt-4, w-full, col-span-2). Your classes always win over Ouro UI's: TextArea merges className last with cn (tailwind-merge), so a class that conflicts with its own replaces it.layout-classes
Appearance comes from props, not classes. In development, a class that sets appearance logs one warning per component instance, naming the prop to use instead. A class sets appearance when, after its variants (hover:, sm:) and any ! are removed, it starts with bg-, text-, border-, rounded-, shadow-, font-, ring-, from-, via-, to-, fill- or stroke-, or is border, rounded, shadow or ring, or has a dark: variant. Text wrapping, overflow and table layout classes (text-wrap, text-ellipsis, border-collapse) are layout and do not warn.
Alignment classes (text-center) warn too: set alignment on a parent element.
Types
ErrorMessage
One error message: text, or a React element for a message that carries a link or other markup.
type ErrorMessage = string | ReactElement;FieldControlProps
Props for FieldControl. className is for layout only.
type FieldControlProps = Omit<BaseField.Control.Props, "className"> & {
className?: string | undefined;
ref?: Ref<HTMLInputElement> | undefined;
};ReadonlyErrorMessages
A read-only message view accepted by fields and Form; mutable input arrays remain valid.
type ReadonlyErrorMessages = ErrorMessage | readonly ErrorMessage[];TextAreaHighlight
A range of a TextArea's text that it marks: words an assistant suggested in the person's draft, a possible spelling mistake. start and end are JavaScript string indexes, and end is exclusive, so a mark covers text.slice(start, end).
interface TextAreaHighlight {
/** Where the mark starts: the index of its first character in the text. */
start: number;
/** Where it ends: the index just after its last character. */
end: number;
/**
* What the mark means, which screen readers hear with the words it marks: "Suggested by
* your assistant", "Possible spelling mistake". Required, so no mark is shown only to sighted people.
*/
label: string;
/**
* The mark's colour, a theme tone. `destructive` and `warning` draw a wavy underline, as
* spelling and grammar checkers do; the others a dotted underline on a faint tint.
* @default "primary"
*/
tone?: "primary" | "secondary" | "destructive" | "success" | "warning" | (string & { 'danger is "destructive"; accent is "primary"; neutral is "secondary"': never }) | undefined;
/** Your own id for the mark. An edit inside a mark splits it, and both pieces keep its id. */
id?: string | undefined;
}Claim sources
- layout-classes: test:registry/ouro/components/ouro/text-field/text-area.test.tsx::TextArea > lets app layout classes win on the root
verify:testExists(registry/ouro/components/ouro/text-field/text-area.test.tsx::TextArea > lets app layout classes win on the root)
