# Textarea

Displays a form textarea for several lines of text.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Label } from "@/components/ui/label"
import { VStack } from "@/components/ui/stack"
import { Textarea } from "@/components/ui/textarea"
import { color, leading, text } from "@/theme/tokens.stylex"

const styles = stylex.create({
  field: { width: "100%", maxWidth: "24rem" },
  hint: { color: color.mutedForeground, fontSize: text.sm, lineHeight: leading.sm },
})

export default function TextareaDemo() {
  return (
    <VStack gap={2} xstyle={styles.field}>
      <Label htmlFor="textarea-demo-message">Message</Label>
      <Textarea id="textarea-demo-message" placeholder="Type your message here." />
      <span {...stylex.props(styles.hint)}>The box grows as you type.</span>
    </VStack>
  )
}
```

## Installation

1. Install the following dependencies:

   ```bash
   npm install @base-ui/react @stylexjs/stylex clsx
   ```

2. Set up the token layer and the StyleX compiler, if you have not already. See
   [Installation](https://carbon.style/docs/installation), and paste `states.ts` from the same page.
   The textarea's field surface, its focus ring and its invalid ring come from
   it. The examples
   caption their textareas with a [Label](https://carbon.style/docs/components/label) or a
   [Field](https://carbon.style/docs/components/field).

   Paste `field-state.ts` from the same installation page.

3. Copy and paste the following code into your project.

   ```tsx title="components/ui/textarea.tsx"
   "use client"

   import { use } from "react"
   import { Field as FieldPrimitive } from "@base-ui/react/field"
   import { FieldStateContext } from "@/lib/field-state"
   import { useRender } from "@base-ui/react/use-render"
   import * as stylex from "@stylexjs/stylex"
   import type { StyleXStyles } from "@stylexjs/stylex"
   import { clsx } from "clsx"

   import { breakpoints } from "@/theme/breakpoints.stylex"
   import { states } from "@/theme/states"
   import { color, duration, ease, leading, radius, space, text } from "@/theme/tokens.stylex"

   const styles = stylex.create({
     base: {
       width: "100%",
       minWidth: 0,
       minHeight: space.s16,
       fieldSizing: "content",
       borderRadius: radius.lg,
       borderWidth: 1,
       borderStyle: "solid",
       paddingInline: space.s2_5,
       paddingBlock: space.s2,
       fontSize: { default: text.base, [breakpoints.md]: text.sm },
       lineHeight: { default: leading.base, [breakpoints.md]: leading.sm },
       color: color.foreground,
       transitionProperty: "color, background-color, border-color, box-shadow",
       transitionDuration: duration.color,
       transitionTimingFunction: ease.standard,
       opacity: { default: 1, ":disabled": 0.5 },
       pointerEvents: { default: null, ":disabled": "none" },
       "::placeholder": { color: color.mutedForeground },
     },
     inGroup: {
       flexGrow: 1,
       flexBasis: 0,
       minHeight: space.s16,
       borderWidth: 0,
       borderRadius: 0,
       backgroundColor: { default: color.transparent, ":disabled": color.transparent },
       boxShadow: { default: "none", ":focus-visible": "none" },
       opacity: { default: 1, ":disabled": 1 },
       resize: "none",
     },
   })

   const textareaStyles = {
     base: [styles.base, states.focusRing, states.fieldSurface],
     invalid: states.invalidRing,
     inGroup: styles.inGroup,
   }

   type TextareaProps = Omit<useRender.ComponentProps<"textarea">, "className"> & {
     xstyle?: StyleXStyles
     className?: string
   }

   function TextareaHost({
     render,
     ref,
     style,
     "aria-invalid": ariaInvalid,
     xstyle,
     className,
     ...props
   }: TextareaProps) {
     const invalid = ariaInvalid != null && ariaInvalid !== false
     const sx = stylex.props(
       styles.base,
       states.focusRing,
       states.fieldSurface,
       invalid && states.invalidRing,
       xstyle,
     )

     return useRender({
       defaultTagName: "textarea",
       render,
       ref,
       props: {
         "data-slot": "textarea",
         ...sx,
         ...props,
         "aria-invalid": invalid || undefined,
         className: clsx(sx.className, className),
         style: { ...sx.style, ...style },
       },
     })
   }

   function Textarea(props: TextareaProps) {
     const field = use(FieldStateContext)
     if (!field) return <TextareaHost {...props} />
     return (
       <FieldPrimitive.Control
         value={props.value}
         defaultValue={props.defaultValue}
         name={props.name}
         disabled={props.disabled}
         render={
           <TextareaHost
             {...props}
             disabled={field.disabled || props.disabled}
             aria-invalid={field.valid === false || props["aria-invalid"]}
           />
         }
       />
     )
   }

   export { Textarea, textareaStyles }
   ```

4. Update the import paths to match your project setup.

## Usage

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

```tsx
<Textarea placeholder="Type your message here." />
```

The control is a plain `<textarea>`, so `value`, `defaultValue`, `onChange`,
`name`, `maxLength` and the rest of the element's attributes work as they
always do. Pair it with a `Label` and match `htmlFor` to the textarea's `id`.

The box sizes itself to its content from a two-line floor, so it grows as the
text does instead of scrolling at a fixed height.

> **Note**
>
> `rows` sets no height here. `field-sizing: content` measures the content
> instead, and the attribute is ignored. Pass a `minHeight` or a `maxHeight`
> through `xstyle` to change where the growing starts and stops.

## Field

Use a [Field](https://carbon.style/docs/components/field) to add a label and a description around
the control.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Field, FieldDescription, FieldLabel } from "@/components/ui/field"
import { Textarea } from "@/components/ui/textarea"

const styles = stylex.create({
  field: { width: "100%", maxWidth: "24rem" },
})

export default function TextareaField() {
  return (
    <Field xstyle={styles.field}>
      <FieldLabel>Message</FieldLabel>
      <FieldDescription>Enter your message below.</FieldDescription>
      <Textarea id="textarea-field-message" placeholder="Type your message here." />
    </Field>
  )
}
```

## Disabled

Set `disabled` on `Field` to disable its control and dim its label, or on a bare control directly.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Field, FieldLabel } from "@/components/ui/field"
import { Textarea } from "@/components/ui/textarea"

const styles = stylex.create({
  field: { width: "100%", maxWidth: "24rem" },
})

export default function TextareaDisabled() {
  return (
    <Field disabled xstyle={styles.field}>
      <FieldLabel>Message</FieldLabel>
      <Textarea id="textarea-disabled-message" placeholder="Type your message here." />
    </Field>
  )
}
```

## Invalid

Set `invalid` on `Field` to style its control, or `aria-invalid` on a bare control. The examples show both.

```tsx
import { Textarea } from "@/components/ui/textarea"
import { Field, FieldLabel, FieldError } from "@/components/ui/field"
import { Label } from "@/components/ui/label"
import { VStack } from "@/components/ui/stack"

export default function TextareaInvalid() {
  return (
    <VStack gap={5} width="100%" maxWidth="24rem">
      <VStack gap={2}>
        <Label htmlFor="textarea-invalid-bare">Message</Label>
        <Textarea
          id="textarea-invalid-bare"
          defaultValue="Hi"
          aria-invalid
          aria-describedby="textarea-invalid-bare-error"
        />
        <FieldError id="textarea-invalid-bare-error">Write at least 20 characters.</FieldError>
      </VStack>
      <Field invalid>
        <FieldLabel>Message</FieldLabel>
        <Textarea defaultValue="Hi" />
        <FieldError>Write at least 20 characters.</FieldError>
      </Field>
    </VStack>
  )
}
```

## Button

Put the textarea and its submit button in a `VStack` for a comment form.

```tsx
import * as stylex from "@stylexjs/stylex"
import { Button } from "@/components/ui/button"
import { HStack, VStack } from "@/components/ui/stack"
import { Textarea } from "@/components/ui/textarea"

const styles = stylex.create({
  form: { width: "100%", maxWidth: "24rem" },
  actions: { justifyContent: "flex-end" },
})

export default function TextareaButton() {
  return (
    <VStack gap={2} xstyle={styles.form}>
      <Textarea placeholder="Type your message here." aria-label="Message" />
      <HStack xstyle={styles.actions}>
        <Button>Send message</Button>
      </HStack>
    </VStack>
  )
}
```

## Input group

To draw one frame around the textarea and a toolbar, wrap it in an
[InputGroup](https://carbon.style/docs/components/input-group). There is no `InputGroupTextarea`.
Give the control `data-slot="input-group-control"` so the group's focus ring
finds it, and `textareaStyles.inGroup` so it drops its own frame.

```tsx
import * as stylex from "@stylexjs/stylex"
import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupText } from "@/components/ui/input-group"
import { Textarea, textareaStyles } from "@/components/ui/textarea"

const styles = stylex.create({
  group: { width: "100%", maxWidth: "24rem" },
  toolbar: { justifyContent: "space-between" },
})

export default function TextareaInputGroup() {
  return (
    <InputGroup xstyle={styles.group}>
      <Textarea
        placeholder="Leave a comment"
        aria-label="Comment"
        data-slot="input-group-control"
        xstyle={textareaStyles.inGroup}
      />
      <InputGroupAddon align="block-end" xstyle={styles.toolbar}>
        <InputGroupText>Markdown supported</InputGroupText>
        <InputGroupButton variant="default">Comment</InputGroupButton>
      </InputGroupAddon>
    </InputGroup>
  )
}
```

## API reference

Pass `render` to replace a part's element or compose it with another component.

`Textarea` renders a `<textarea>` and accepts every attribute the element
takes.

### Textarea

| Prop           | Type           | Default |
| -------------- | -------------- | ------- |
| `disabled`     | `boolean`      | `false` |
| `aria-invalid` | `boolean`      | —       |
| `placeholder`  | `string`       | —       |
| `xstyle`       | `StyleXStyles` | —       |
| `className`    | `string`       | —       |

### textareaStyles

The style maps behind the component. Spread them onto any element.

| Key       | What it is                                                  |
| --------- | ----------------------------------------------------------- |
| `base`    | The control: frame, fill, type scale, placeholder, disabled |
| `invalid` | The destructive border and ring                             |
| `inGroup` | The frameless form a textarea takes inside an `InputGroup`  |

`base` is three styles, in merge order: the control's own, the shared
`states.focusRing`, and `states.fieldSurface`, which is the hairline edge, the
fill and the drop. `invalid` is `states.invalidRing` from `theme/states.ts`. Set
a border of your own through `--carbon-border-color` rather than `borderColor`.
A flat `borderColor` replaces the property with all of its conditions, so the ring loses its focus colour.

### Data attributes

| Attribute              | On          | Effect                                          |
| ---------------------- | ----------- | ----------------------------------------------- |
| `data-slot="textarea"` | the control | Targets the control from a parent               |
| `aria-invalid`         | the control | Set from the prop, and dropped when it is false |
