Skip to main content
PolyUI/docs

Textarea

A multi-line text input with support for controlled mode, disabled state, and custom row count.

Installation

bash
npx polyui add textarea

Basic

tsx
import { Textarea } from "@polyui/react/textarea"
export function TextareaBasic() {
  return <Textarea placeholder="Type your message here." className="w-full max-w-xs" />
}

With Label

tsx
import { useId } from "react"
import { Textarea } from "@polyui/react/textarea"
import { Label } from "@polyui/react/label"
export function TextareaWithLabel() {
  const id = useId()
  return (
    <div className="w-full max-w-xs space-y-2">
      <Label htmlFor={id}>Your message</Label>
      <Textarea placeholder="Type your feedback here." id={id} />
    </div>
  )
}

Invalid

Please provide useful feedback.

tsx
import { useId } from "react"
import { Textarea } from "@polyui/react/textarea"
import { Label } from "@polyui/react/label"
export function TextareaInvalid() {
  const id = useId()
  return (
    <div className="w-full max-w-xs space-y-2">
      <Label htmlFor={id}>Invalid</Label>
      <Textarea aria-invalid placeholder="Type your feedback here." id={id} />
      <p className="text-destructive text-xs">Please provide useful feedback.</p>
    </div>
  )
}

Placeholder

tsx
import { useId } from "react"
import { Textarea } from "@polyui/react/textarea"
import { Label } from "@polyui/react/label"
export function TextareaPlaceholder() {
  const id = useId()
  return (
    <div className="w-full max-w-xs space-y-2">
      <Label htmlFor={id}>Placeholder</Label>
      <Textarea id={id} placeholder="Enter your message..." />
    </div>
  )
}

Disabled

tsx
import { useId } from "react"
import { Textarea } from "@polyui/react/textarea"
import { Label } from "@polyui/react/label"
export function TextareaDisabled() {
  const id = useId()
  return (
    <div className="w-full max-w-xs space-y-2">
      <Label htmlFor={id}>Disabled</Label>
      <Textarea placeholder="Type your feedback here." disabled id={id} />
    </div>
  )
}

Read Only

tsx
import { useId } from "react"
import { Textarea } from "@polyui/react/textarea"
import { Label } from "@polyui/react/label"
export function TextareaReadOnly() {
  const id = useId()
  return (
    <div className="w-full max-w-xs space-y-2">
      <Label htmlFor={id}>Read Only</Label>
      <Textarea
        className="read-only:bg-muted"
        defaultValue="Read only text"
        placeholder="Type your feedback here."
        id={id}
        readOnly
      />
    </div>
  )
}

With Button

tsx
import { useId } from "react"
import { Textarea } from "@polyui/react/textarea"
import { Label } from "@polyui/react/label"
import { Button } from "@polyui/react/button"
export function TextareaWithButton() {
  const id = useId()
  return (
    <div className="w-full max-w-xs space-y-2">
      <Label htmlFor={id}>With Button</Label>
      <Textarea id={id} placeholder="Type your feedback here." />
      <Button size="sm">Submit Feedback</Button>
    </div>
  )
}

Auto Grow

tsx
import { useId } from "react"
import { Textarea } from "@polyui/react/textarea"
import { Label } from "@polyui/react/label"
export function TextareaAutoGrow() {
  const id = useId()
  return (
    <div className="w-full max-w-xs space-y-2">
      <Label htmlFor={id}>Auto Grow</Label>
      <Textarea
        id={id}
        placeholder="Type your feedback here."
        className="field-sizing-content max-h-30 min-h-0 resize-none py-1.75"
      />
    </div>
  )
}

Filled

tsx
import { useId } from "react"
import { Textarea } from "@polyui/react/textarea"
import { Label } from "@polyui/react/label"
export function TextareaFilled() {
  const id = useId()
  return (
    <div className="w-full max-w-xs space-y-2">
      <Label htmlFor={id}>Filled</Label>
      <Textarea className="bg-muted border-transparent shadow-none" placeholder="Type your feedback here." id={id} />
    </div>
  )
}

Sizes

tsx
import { Textarea } from "@polyui/react/textarea"
export function TextareaSizes() {
  return (
    <div className="w-full max-w-xs space-y-2">
      <Textarea className="min-h-10 py-1.5" placeholder="Small" />
      <Textarea placeholder="Medium" />
      <Textarea className="min-h-20 py-2.5" placeholder="Large" />
    </div>
  )
}

Start Icon

Address
tsx
import { useId } from "react"
import { Textarea } from "@polyui/react/textarea"
import { Label } from "@polyui/react/label"
import { HomeIcon } from "lucide-react"
export function TextareaStartIcon() {
  const id = useId()
  return (
    <div className="w-full max-w-xs space-y-2">
      <Label htmlFor={id}>Start Icon</Label>
      <div className="relative">
        <div className="text-muted-foreground pointer-events-none absolute top-2.5 left-0 flex items-center justify-center pl-3 peer-disabled:opacity-50">
          <HomeIcon className="size-4" />
          <span className="sr-only">Address</span>
        </div>
        <Textarea id={id} placeholder="Address" className="peer pl-9" />
      </div>
    </div>
  )
}

End Icon

Address
tsx
import { useId } from "react"
import { Textarea } from "@polyui/react/textarea"
import { Label } from "@polyui/react/label"
import { HomeIcon } from "lucide-react"
export function TextareaEndIcon() {
  const id = useId()
  return (
    <div className="w-full max-w-xs space-y-2">
      <Label htmlFor={id}>End Icon</Label>
      <div className="relative">
        <div className="text-muted-foreground pointer-events-none absolute top-2.5 right-0 flex items-center justify-center pr-3 peer-disabled:opacity-50">
          <HomeIcon className="size-4" />
          <span className="sr-only">Address</span>
        </div>
        <Textarea id={id} placeholder="Address" className="peer pr-9" />
      </div>
    </div>
  )
}

Overlapping Label

tsx
import { useId } from "react"
import { Textarea } from "@polyui/react/textarea"
import { Label } from "@polyui/react/label"
export function TextareaOverlappingLabel() {
  const id = useId()
  return (
    <div className="relative w-full max-w-xs space-y-2">
      <Label
        htmlFor={id}
        className="bg-background text-foreground absolute top-0 left-2 z-10 block -translate-y-1/2 px-1 text-xs font-medium group-has-disabled:opacity-50"
      >
        Overlapping Label
      </Label>
      <Textarea id={id} className="!bg-background" />
    </div>
  )
}

Floating Label

tsx
import { useId } from "react"
import { Textarea } from "@polyui/react/textarea"
import { Label } from "@polyui/react/label"
export function TextareaFloatingLabel() {
  const id = useId()
  return (
    <div className="group relative w-full max-w-xs space-y-2">
      <label
        htmlFor={id}
        className="origin-start text-muted-foreground/70 group-focus-within:text-foreground has-[+textarea:not(:placeholder-shown)]:text-foreground absolute top-0 block translate-y-2 cursor-text px-2 text-sm transition-all group-focus-within:pointer-events-none group-focus-within:-translate-y-1/2 group-focus-within:cursor-default group-focus-within:text-xs group-focus-within:font-medium has-[+textarea:not(:placeholder-shown)]:pointer-events-none has-[+textarea:not(:placeholder-shown)]:-translate-y-1/2 has-[+textarea:not(:placeholder-shown)]:cursor-default has-[+textarea:not(:placeholder-shown)]:text-xs has-[+textarea:not(:placeholder-shown)]:font-medium"
      >
        <span className="bg-background inline-flex px-1">Floating Label</span>
      </label>
      <Textarea id={id} placeholder=" " className="!bg-background" />
    </div>
  )
}

Inset Label

tsx
import { useId } from "react"
import { Label } from "@polyui/react/label"
export function TextareaInsetLabel() {
  const id = useId()
  return (
    <div className="border-input bg-background focus-within:border-ring focus-within:ring-ring/50 has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40 has-aria-invalid:border-destructive relative w-full max-w-xs rounded-md border shadow-xs transition-[color,box-shadow] outline-none focus-within:ring-[3px] has-disabled:pointer-events-none has-disabled:cursor-not-allowed has-disabled:opacity-50">
      <label htmlFor={id} className="text-foreground block px-3 pt-1 text-xs font-medium">
        Inset Label
      </label>
      <textarea
        id={id}
        className="text-foreground placeholder:text-muted-foreground/70 flex min-h-14 w-full px-3 pb-2 text-sm focus-visible:outline-none"
      />
    </div>
  )
}

No Resize

tsx
import { useId } from "react"
import { Textarea } from "@polyui/react/textarea"
import { Label } from "@polyui/react/label"
export function TextareaNoResize() {
  const id = useId()
  return (
    <div className="w-full max-w-xs space-y-2">
      <Label htmlFor={id}>No Resize</Label>
      <Textarea id={id} placeholder="Type your feedback here." className="[resize:none]" />
    </div>
  )
}

Character Count

200 characters left

tsx
import { useId, useState } from "react"
import { Textarea } from "@polyui/react/textarea"
import { Label } from "@polyui/react/label"
export function TextareaCharacterCount() {
  const [value, setValue] = useState("")
  const id = useId()
  const handleChange = (e: ChangeEvent<HTMLTextAreaElement>) => {
    if (e.target.value.length <= MAX_LENGTH) setValue(e.target.value)
  }
  return (
    <div className="w-full max-w-xs space-y-2">
      <Label htmlFor={id}>Character Count</Label>
      <Textarea
        placeholder="Type your feedback here."
        value={value}
        maxLength={MAX_LENGTH}
        onChange={handleChange}
        id={id}
      />
      <p className="text-muted-foreground text-xs">
        <span className="tabular-nums">{MAX_LENGTH - value.length}</span> characters left
      </p>
    </div>
  )
}

Props

属性类型默认值说明
placeholderstringPlaceholder text shown when the textarea is empty.
disabledbooleanfalseDisables the textarea, preventing interaction and reducing visual opacity.
rowsnumber5Initial visible row count. Defaults to 5.
valuestringThe controlled text content of the textarea.
onChange(e: React.ChangeEvent<HTMLTextAreaElement>) => voidCallback fired when the text content changes.