Skip to content
Facade UI
uiatom

Textarea

A multi-line text field built on Base UI's Field, so the label, help text and error message are connected to the control. Same border and focus ring as Input.

Open in v0 (opens in a new tab)

Preview

Open full width (opens in a new tab)
Preview width

Installation

Package manager
pnpm dlx shadcn@latest add @facade/textarea

This component needs @base-ui-components/react@1.0.0-rc.0, utils. The CLI installs them for you.

Usage

This is the code for the preview above. To install it as a file, run npx shadcn@latest add @facade/textarea-demo.

demos/textarea.tsx
"use client"

/** `"use client"` only for the submit handler that keeps the demo on the page. */

import { Button } from "@registry/ui/button"
import { Input } from "@registry/ui/input"
import { Textarea } from "@registry/ui/textarea"

export function Demo() {
  return (
    <form
      onSubmit={(event) => event.preventDefault()}
      noValidate
      className="flex max-w-md flex-col gap-5"
    >
      <Input label="Name" name="name" autoComplete="name" required />
      <Textarea
        label="Message"
        name="message"
        rows={5}
        description="Tell us what you are building."
        required
      />
      <Textarea
        label="Notes"
        name="notes"
        rows={2}
        resize="none"
        defaultValue="Please call after 3pm, or before 9am on weekdays, or any time at weekends."
        error="Keep notes under 60 characters."
      />
      <Button type="submit" className="self-start">
        Send
      </Button>
    </form>
  )
}

Source

These are the files the CLI copies into your project.

components/ui/textarea.tsx
/**
 * Textarea — a multi-line text field built on Base UI's Field, so the label,
 * description and error are wired to the control the same way `Input` wires
 * them.
 *
 * Base UI's `Input` is `Field.Control`; here `Field.Control` renders a
 * `<textarea>` through its `render` prop, which keeps the generated ids, the
 * `aria-*` links and the validation ref.
 *
 * a11y notes worth keeping:
 *
 *  - The border uses `--input`, held at 3:1 against the background.
 *  - `Field.Root invalid` marks the control `aria-invalid` and turns the
 *    border red for an error passed in from outside, such as one from a server.
 *  - "required" is spelled out in the label, as in `Input`.
 *  - At least 44px tall, and it can be resized vertically unless `resize` is
 *    `none`, so long answers stay readable without a scrollbar.
 *
 * Dependencies: @base-ui-components/react, react, @/lib/utils.
 */

import { Field } from "@base-ui-components/react/field"
import type { ComponentPropsWithoutRef, ReactNode } from "react"

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

export interface TextareaProps extends ComponentPropsWithoutRef<"textarea"> {
  /** Required. Use `hideLabel` if it should not be visible. */
  label: ReactNode
  /** Hides the label visually but keeps it for screen readers. */
  hideLabel?: boolean
  /** Help text below the field. It is linked to the field for you. */
  description?: ReactNode
  /** Error message. Marks the field invalid and is announced with it. */
  error?: ReactNode
  /** Visible lines of text. */
  rows?: number
  /** `vertical` lets the user drag the height; `none` fixes it. */
  resize?: "vertical" | "none"
  /** Classes for the outer Field.Root. */
  className?: string
  /** Classes for the `<textarea>` itself. */
  textareaClassName?: string
}

export function Textarea({
  label,
  hideLabel = false,
  description,
  error,
  rows = 4,
  resize = "vertical",
  className,
  textareaClassName,
  name,
  value,
  defaultValue,
  disabled,
  required,
  ...props
}: TextareaProps) {
  return (
    <Field.Root
      name={name}
      invalid={Boolean(error)}
      className={cn("flex w-full flex-col gap-1.5", className)}
    >
      <Field.Label
        className={cn("text-foreground text-sm font-medium", hideLabel && "sr-only")}
      >
        {label}
        {required ? <span className="text-muted-foreground"> (required)</span> : null}
      </Field.Label>

      {/* Field reads the form props; the rest are textarea attributes. */}
      <Field.Control
        name={name}
        value={value}
        defaultValue={defaultValue}
        disabled={disabled}
        required={required}
        render={<textarea rows={rows} {...props} />}
        className={cn(
          "border-input bg-background text-foreground placeholder:text-muted-foreground",
          "min-h-11 w-full rounded-md border px-3.5 py-2.5 text-base leading-6",
          "duration-facade-fast ease-facade-out transition-[border-color,box-shadow]",
          "focus-visible:ring-ring focus-visible:border-ring outline-none focus-visible:ring-2",
          "disabled:cursor-not-allowed disabled:opacity-60",
          "data-[invalid]:border-destructive data-[invalid]:focus-visible:ring-destructive",
          resize === "none" ? "resize-none" : "resize-y",
          textareaClassName,
        )}
      />

      {description ? (
        <Field.Description className="text-muted-foreground text-pretty text-sm">
          {description}
        </Field.Description>
      ) : null}

      {/* `match` shows an error from outside, which Base UI's own validity
          state knows nothing about. Without one, no children are passed, so
          Base UI shows the browser's own message for a failed check. */}
      <Field.Error
        className="text-destructive text-pretty text-sm"
        match={Boolean(error) || undefined}
        {...(error ? { children: error } : {})}
      />
    </Field.Root>
  )
}

Props

TextareaProps

Extends ComponentPropsWithoutRef<"textarea">.

Props for TextareaProps
PropTypeDefault
label*RequiredRequired. Use `hideLabel` if it should not be visible.ReactNode—
hideLabelHides the label visually but keeps it for screen readers.booleanfalse
descriptionHelp text below the field. It is linked to the field for you.ReactNode—
errorError message. Marks the field invalid and is announced with it.ReactNode—
rowsVisible lines of text.number4
resize`vertical` lets the user drag the height; `none` fixes it."vertical" | "none""vertical"
classNameClasses for the outer Field.Root.string—
textareaClassNameClasses for the `<textarea>` itself.string—