Forms

TextArea

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.

It grows as you type.

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-monorepo
npx shadcn@latest add ourostack/ouro-ui/ouro-base#v1.2.13

Then 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.2.13

Or, with the @ouro namespace set up:

npx shadcn@latest add @ouro/ouro-base @ouro/text-area

Its 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.2.13 ourostack/ouro-ui/text-area-tests#v1.2.13
import { 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} />
// 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.

Use instead when

  • Use TextField instead for one line, and Field with FieldControl for a control that is not text.

Props

Type: TextAreaProps.

autoGrow
Type: boolean
Grows with the content, from rows lines up to maxRows, 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.
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.
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 required and aria-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. md is 16px on phones and steps down to 14px from the sm breakpoint (640px). lg is 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 onValueChange or onChange.
variant
Type: "outline" | "plain"
Default: "outline"
The frame. outline draws the bordered box. plain draws 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 uses autoGrow: <TextArea autoGrow>.
  • autosize: Not a prop. Ouro UI uses autoGrow: <TextArea autoGrow>.
  • errorMessage: Not a prop. Ouro UI uses error: <TextArea error="Required.">.
  • helperText: Not a prop. Ouro UI uses description: <TextArea description="Only you see these.">.
  • invalid: Not a prop. error marks the field invalid: <TextArea error="What is wrong">.
  • isDisabled: Not a prop. Ouro UI uses disabled: <TextArea disabled>.
  • isInvalid: Not a prop. error marks the field invalid: <TextArea error="What is wrong">.
  • isRequired: Not a prop. Ouro UI uses required: <TextArea required>.
  • minRows: Not a prop. rows is 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[];

Claim sources