# Textarea

> Collect text that may span more than one line.

Source: https://www.honestui.com/docs/components/textarea

```tsx
import { Textarea } from "@/components/honest-ui/ui/textarea"

export function TextareaDemo() {
  return <Textarea className="w-full max-w-64" placeholder="Type your message here" />
}

```

## Overview [#overview]

Textarea collects free-form text that can outgrow one line: descriptions, messages, notes, code snippets. It is the multi-line sibling of Input and shares its contract — a permanent label above, placeholder showing format only, native attributes passed straight through.

Use it when people will type more than roughly a sentence. For anything shorter, an Input respects their time; a tall empty box implies an essay is expected.

## Anatomy [#anatomy]

A textarea has a value, optional placeholder, size, variant, and state. Unlike Input there is no `type` — but `maxLength`, `minLength`, `required`, `disabled`, `readOnly`, and `name` all pass through natively.

## Behavior [#behavior]

**Growth.** The box starts at a fixed height and scrolls once content exceeds it; it does not grow automatically. If watching all of their text matters (short comments), size it generously up front rather than promising auto-grow the component does not do.

**Resize.** The browser's resize handle stays available. Constrain it with `resize` classes if the layout truly cannot flex — but a textarea nobody may enlarge is usually a layout bug wearing a component.

**Line handling.** Enter inserts newlines; text wraps within the box. Values include those newline characters, so trim and normalize on submit where your backend cares.

## States [#states]

**Disabled** skips focus, blocks editing, and excludes the value from submission. **Read-only** keeps the content selectable and copyable while still submitting — prefer it for computed or imported text that must travel with the form. **Invalid** follows `aria-invalid`: danger border plus an error message linked through Field or `aria-describedby` that says what to fix.

Placeholder styling matches Input: muted foreground tokens that adapt to dark mode automatically.

## Accessibility [#accessibility]

The visible label names the field (`Label` + `htmlFor`). Standard textarea keys apply — arrows move within the text, <kbd>Enter</kbd> adds lines, <kbd>Tab</kbd> leaves (it does not insert a tab character). Focus shows as a border change using accent tokens.

Height clears comfortable touch sizing, and the resize handle gives pointer users another way to fit their content. Long values scroll vertically rather than truncating; nothing a person typed is ever visually cut off without a way to reach it. RTL text mirrors correctly because padding uses logical properties.

## Installation [#installation]


  

  
    <CliBlock commands="[&#x22;textarea&#x22;]" />
  

  
    
      
        Install the following dependencies:
      

      ```bash
      npm install @base-ui-components/react
      ```

      
        Copy and paste the following code into your project.
      

      ### components/ui/textarea.tsx

```tsx
import * as React from "react"

import { cn } from "@/lib/utils"

function Textarea({
  className,
  size = "large",
  variant = "default",
  ...props
}: React.ComponentProps<"textarea"> & {
  size?: "sm" | "small" | "default" | "lg" | "large" | number
  variant?: "default" | "borderless"
}) {
  return (
    <textarea
      data-slot="textarea"
      className={cn(
        "m-0 box-border h-auto w-full appearance-none overflow-auto rounded-[var(--hui-radius-2)] border-[0.5px] border-[var(--hui-color-border-base-tertiary)] bg-[var(--hui-color-background-base-primary)] text-[var(--hui-color-foreground-base-primary)] outline-none [font-size:var(--hui-font-size-small)] [line-height:var(--hui-line-height-small)] placeholder:text-[var(--hui-color-foreground-base-tertiary)] placeholder:[font-size:var(--hui-font-size-small)] placeholder:[font-weight:var(--hui-font-weight-regular)] placeholder:[line-height:var(--hui-line-height-small)] read-only:bg-[var(--hui-color-background-base-secondary)] disabled:cursor-not-allowed disabled:opacity-50 data-disabled:cursor-not-allowed data-disabled:opacity-50 data-invalid:border-[var(--hui-color-border-danger-emphasis)] aria-invalid:border-[var(--hui-color-border-danger-emphasis)] [&:focus:not(:disabled)]:border-[var(--hui-color-border-accent-emphasis)] [&:focus:not(:disabled)]:bg-[var(--hui-color-background-base-primary)] data-invalid:focus:border-[var(--hui-color-border-danger-emphasis-hover)] aria-invalid:focus:border-[var(--hui-color-border-danger-emphasis-hover)] motion-safe:[transition:var(--hui-transition-interactive)]",
        (size === "large" || size === "lg" || size === "default") &&
          "p-[var(--hui-space-3)]",
        (size === "small" || size === "sm") && "p-[var(--hui-space-2)]",
        variant === "borderless" &&
          "border-transparent [&:focus:not(:disabled)]:border-transparent! [&:focus-visible:not(:disabled)]:shadow-[var(--hui-focus-ring-shadow)]",
        className
      )}
      {...props}
    />
  )
}

export { Textarea }

```

      
        Update the import paths to match your project setup.
      
    
  


## Usage [#usage]

```tsx
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
```

```tsx
<Label htmlFor={id}>Release notes</Label>
<Textarea id={id} name="notes" rows={4} maxLength={500} />
```

## Don't do this [#dont-do-this]

### Counting characters only after they are lost [#counting-characters-only-after-they-are-lost]

```tsx
// Bad
<Textarea maxLength={200} />
// no counter anywhere
```

```tsx
// Good
<Textarea maxLength={200} aria-describedby="count" />
<p id="count" aria-live="polite">
  {200 - value.length} characters left
</p>
```

A silent `maxLength` eats keystrokes: past the limit, typing does nothing and the person concludes the keyboard broke. Announce remaining characters politely so the wall arrives before they hit it — and let the counter say what still fits, not just that they failed.

### Disabling spellcheck by default [#disabling-spellcheck-by-default]

```tsx
// Bad
<Textarea spellCheck={false} />
```

```tsx
// Good
<Textarea /> // browser default: spellcheck on for prose
```

Prose fields benefit from underlines pointing at typos; turning them off wholesale assumes every entry is a URL. Opt out per-field for identifiers and code, not for every description box on the page.

## Examples [#examples]

### Disabled and read-only states [#disabled-and-read-only-states]

```tsx
import { Textarea } from "@/components/honest-ui/ui/textarea"

export function TextareaDisabled() {
  return <Textarea className="w-full max-w-64" placeholder="Can't type here" disabled />
}

```

### Inside a form [#inside-a-form]

```tsx
"use client";

import * as React from "react";

import { Button } from "@/components/honest-ui/ui/button";
import {
  Field,
  FieldControl,
  FieldError,
  FieldLabel,
} from "@/components/honest-ui/ui/field";
import { Form } from "@/components/honest-ui/ui/form";
import { Textarea } from "@/components/honest-ui/ui/textarea";

export function TextareaForm() {
  const [status, setStatus] = React.useState("");
  const onSubmit = (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    const formData = new FormData(e.currentTarget);
    const message = String(formData.get("message") || "");
    setStatus(`Message ready to send (${message.length} characters).`);
  };

  return (
    <Form onSubmit={onSubmit} className="grid w-full max-w-64 gap-4">
      <Field>
        <FieldLabel>Message</FieldLabel>
        <FieldControl
          name="message"
          placeholder="Type your message here"
          required
          render={(props) => <Textarea {...props} />}
        />
        <FieldError>This field is required.</FieldError>
      </Field>
      <Button type="submit">Send message</Button>
      <p className="text-sm text-muted-foreground" role="status">
        {status}
      </p>
    </Form>
  );
}

```

### With label [#with-label]

```tsx
import { useId } from "react"

import { Label } from "@/components/honest-ui/ui/label"
import { Textarea } from "@/components/honest-ui/ui/textarea"

export function TextareaWithLabel() {
  const id = useId()
  return (
    <div className="w-full max-w-64 flex flex-col items-start gap-2">
      <Label htmlFor={id}>Message</Label>
      <Textarea id={id} placeholder="Type your message here" />
    </div>
  )
}

```

## API reference [#api-reference]

Accepts all native `<textarea>` props. Honest UI additions:

| Prop      | Values                                                       | Default     |
| --------- | ------------------------------------------------------------ | ----------- |
| `size`    | `"sm"`, `"small"`, `"default"`, `"lg"`, `"large"`, or number | `"large"`   |
| `variant` | `"default"`, `"borderless"`                                  | `"default"` |

Validation state follows `aria-invalid`; disabled and read-only use the native attributes.
