vera logoVera UI
ComponentsForm Components

Textarea

A textarea component for multi-line text input with automatic resizing support. Built with accessibility and user experience in mind, featuring modern styling and seamless integration with forms.

Installation

pnpm add @helgadigitals/vera-ui
npm install @helgadigitals/vera-ui
yarn add @helgadigitals/vera-ui

Usage

import { Textarea } from "@helgadigitals/vera-ui"export default function Example() {return (  <Textarea placeholder="Type your message here..." className="w-[300px]" />)}

Examples

With Label

import { Textarea, Label } from "@helgadigitals/vera-ui"export default function BasicExample() {return (  <div className="space-y-2 w-[300px]">    <Label htmlFor="message">Message</Label>    <Textarea      id="message"      placeholder="Enter your message..."      rows={4}      className="w-[300px]"    />  </div>)}

Disabled State

import { Textarea } from "@helgadigitals/vera-ui"export default function DisabledExample() {return (  <div className="space-y-4 w-[300px]">    <Textarea placeholder="Disabled textarea" disabled className="w-[300px]" />    <Textarea placeholder="Readonly textarea" readOnly defaultValue="This is read-only content" className="w-[300px]" />  </div>)}

Controlled State

Character count: 0

import { useState } from "react"import { Textarea, Label } from "@helgadigitals/vera-ui"export default function ControlledExample() {const [value, setValue] = useState("")return (  <div className="space-y-4 w-full max-w-md">    <div className="space-y-2">      <Label htmlFor="controlled">Description</Label>      <Textarea        id="controlled"        value={value}        onChange={(e) => setValue(e.target.value)}        placeholder="Enter a description..."        className="w-full"      />    </div>        <p className="text-sm text-muted-foreground">      Character count: {value.length}    </p>  </div>)}

With Character Limit

Keep it concise and engaging

0/200

import { useState } from "react"import { Textarea, Label } from "@helgadigitals/vera-ui"export default function CharacterLimitExample() {const [value, setValue] = useState("")const maxLength = 200return (  <div className="space-y-2 w-[300px]">    <Label htmlFor="limited">Bio (max 200 characters)</Label>    <Textarea      id="limited"      value={value}      onChange={(e) => setValue(e.target.value)}      placeholder="Tell us about yourself..."      maxLength={maxLength}      className={`w-[300px] ${value.length >= maxLength ? "border-yellow-500" : ""}`}    />    <div className="flex justify-between text-sm">      <span className="text-muted-foreground">        Keep it concise and engaging      </span>      <span className={`${        value.length >= maxLength ? "text-yellow-600" : "text-muted-foreground"      }`}>        {value.length}/{maxLength}      </span>    </div>  </div>)}

Different Sizes

import { Textarea, Label } from "@helgadigitals/vera-ui"export default function SizesExample() {return (  <div className="space-y-6 w-full max-w-md">    {/* Small textarea */}    <div className="space-y-2">      <Label htmlFor="small">Small Textarea</Label>      <Textarea        id="small"        placeholder="Small textarea..."        className="min-h-[80px] w-full"      />    </div>    {/* Default textarea */}    <div className="space-y-2">      <Label htmlFor="default">Default Textarea</Label>      <Textarea        id="default"        placeholder="Default textarea..."        className="w-full"      />    </div>    {/* Large textarea */}    <div className="space-y-2">      <Label htmlFor="large">Large Textarea</Label>      <Textarea        id="large"        placeholder="Large textarea..."        className="min-h-[150px] w-full"      />    </div>  </div>)}

Disabled and Read-only States

import { Textarea, Label } from "@helgadigitals/vera-ui"export default function StatesExample() {return (  <div className="space-y-6 w-full max-w-md">    {/* Disabled textarea */}    <div className="space-y-2">      <Label htmlFor="disabled">Disabled Textarea</Label>      <Textarea        id="disabled"        placeholder="This textarea is disabled"        disabled        className="w-full"      />    </div>    {/* Read-only textarea */}    <div className="space-y-2">      <Label htmlFor="readonly">Read-only Textarea</Label>      <Textarea        id="readonly"        value="This textarea is read-only and cannot be edited."        readOnly        className="w-full"      />    </div>  </div>)}

Form Integration

import { useForm, Controller } from "react-hook-form"import { Textarea, Label, Button } from "@helgadigitals/vera-ui"export default function FormExample() {const { control, handleSubmit, formState: { errors }, watch } = useForm()const watchedValues = watch()const onSubmit = (data) => {  console.log(data)}return (  <form onSubmit={handleSubmit(onSubmit)} className="space-y-6 w-full max-w-md">    <div className="space-y-2">      <Label htmlFor="feedback">Feedback *</Label>      <Controller        name="feedback"        control={control}        rules={{          required: "Feedback is required",          minLength: {            value: 10,            message: "Feedback must be at least 10 characters"          }        }}        render={({ field }) => (          <Textarea            id="feedback"            placeholder="Please share your feedback..."            className={`w-full ${errors.feedback ? "border-destructive" : ""}`}            {...field}          />        )}      />      {errors.feedback && (        <p className="text-sm text-destructive">          {errors.feedback.message}        </p>      )}    </div>    <div className="space-y-2">      <Label htmlFor="comments">Additional Comments</Label>      <Controller        name="comments"        control={control}        render={({ field }) => (          <Textarea            id="comments"            placeholder="Any additional comments (optional)..."            className="w-full"            {...field}          />        )}      />    </div>    <Button type="submit" className="w-full">Submit Feedback</Button>  </form>)}

Auto-resizing Textarea

Lines: 1 | Characters: 0

import { useState, useRef, useEffect } from "react"import { Textarea, Label } from "@helgadigitals/vera-ui"function AutoResizeTextarea({ value, onChange, ...props }) {const textareaRef = useRef(null)useEffect(() => {  if (textareaRef.current) {    // Reset height to get accurate scrollHeight    textareaRef.current.style.height = 'auto'    // Set height to scrollHeight    textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`  }}, [value])return (  <Textarea    ref={textareaRef}    value={value}    onChange={onChange}    className="resize-none overflow-hidden w-full"    {...props}  />)}export default function AutoResizeExample() {const [value, setValue] = useState("")return (  <div className="space-y-2 w-full max-w-md">    <Label htmlFor="auto-resize">Auto-resizing Textarea</Label>    <AutoResizeTextarea      id="auto-resize"      value={value}      onChange={(e) => setValue(e.target.value)}      placeholder="Start typing and watch the textarea grow..."    />  </div>)}

With Helper Text and Error

Provide a detailed description of your product's features and benefits.

0/500
import { useState } from "react"import { Textarea, Label } from "@helgadigitals/vera-ui"export default function WithHelperTextExample() {const [value, setValue] = useState("")const [error, setError] = useState("")const validateInput = (text) => {  if (text.length > 0 && text.length < 5) {    setError("Description must be at least 5 characters long")  } else if (text.length > 500) {    setError("Description cannot exceed 500 characters")  } else {    setError("")  }}const handleChange = (e) => {  const newValue = e.target.value  setValue(newValue)  validateInput(newValue)}return (  <div className="space-y-2 w-full max-w-md">    <Label htmlFor="description">Product Description</Label>    <Textarea      id="description"      value={value}      onChange={handleChange}      placeholder="Describe your product..."      className={`w-full ${error ? "border-destructive focus-visible:ring-destructive/20" : ""}`}    />    <div className="flex justify-between text-sm">      <div className="space-y-1">        <p className="text-muted-foreground">          Provide a detailed description of your product's features and benefits.        </p>        {error && (          <p className="text-destructive">{error}</p>        )}      </div>      <span className="text-muted-foreground shrink-0">        {value.length}/500      </span>    </div>  </div>)}

Message Composer

import { useState } from "react"import { Send } from "lucide-react"import { Textarea, Button } from "@helgadigitals/vera-ui"export default function MessageComposerExample() {const [message, setMessage] = useState("")const handleSend = () => {  if (message.trim()) {    console.log("Sending message:", message)    setMessage("")  }}const handleKeyDown = (e) => {  if (e.key === "Enter" && (e.metaKey || e.ctrlKey)) {    e.preventDefault()    handleSend()  }}return (  <div className="border rounded-lg p-4 space-y-3 w-full max-w-md">    <Textarea      value={message}      onChange={(e) => setMessage(e.target.value)}      onKeyDown={handleKeyDown}      placeholder="Type your message... (Cmd/Ctrl + Enter to send)"      className="border-0 shadow-none resize-none focus-visible:ring-0 p-0 w-full"      rows={3}    />        <div className="flex justify-between items-center">      <p className="text-xs text-muted-foreground">        {message.length > 0 && `${message.length} characters`}      </p>      <Button        onClick={handleSend}        disabled={!message.trim()}        size="sm"        className="gap-2"      >        <Send className="h-4 w-4" />        Send      </Button>    </div>  </div>)}

Code Editor Style

Lines: 5 | Characters: 82

import { useState } from "react"import { Textarea, Label } from "@helgadigitals/vera-ui"export default function CodeEditorExample() {const [code, setCode] = useState(`function greet(name) {return \`Hello, \${name}!\`;}console.log(greet("World"));`)return (  <div className="space-y-2 w-full max-w-md">    <Label htmlFor="code">JavaScript Code</Label>    <Textarea      id="code"      value={code}      onChange={(e) => setCode(e.target.value)}      placeholder="Enter your code here..."      className="font-mono text-sm min-h-[200px] bg-muted/30 w-full"      spellCheck={false}    />  </div>)}

API Reference

Props

The Textarea component accepts all standard HTML textarea attributes:

Prop

Type

Accessibility

The Textarea component provides full accessibility support:

  • Keyboard Navigation: Standard textarea keyboard interactions
  • Screen Readers: Proper semantic textarea element with labels
  • Focus Management: Clear focus indicators and proper focus handling
  • Validation: Supports HTML5 validation attributes and custom validation

Labeling: Always provide proper labels for textareas using the label element with htmlFor attribute or aria-label for screen reader accessibility.

// Good: Properly labeled textarea
<div className="space-y-2">
  <Label htmlFor="description">Description</Label>
  <Textarea id="description" />
</div>

// Also good: Using aria-label
<Textarea aria-label="Enter your comments" />

Field Sizing

The textarea component includes modern CSS field-sizing: content support for automatic content-based sizing where supported by browsers:

// The textarea will automatically adjust height based on content
<Textarea placeholder="This will auto-resize..." />

Best Practices

Provide Clear Labels

Always include descriptive labels and helpful placeholder text:

// Good: Clear label and helpful placeholder
<div className="space-y-2">
  <Label htmlFor="feedback">Customer Feedback</Label>
  <Textarea
    id="feedback"
    placeholder="Please share your experience with our product..."
  />
</div>

Set Appropriate Size

Use appropriate sizing for the expected content:

// Short content
<Textarea className="min-h-[80px]" placeholder="Brief comment..." />

// Long content
<Textarea className="min-h-[200px]" placeholder="Detailed description..." />

Include Character Limits

Show character counts for limited inputs:

<div className="space-y-2">
  <Textarea maxLength={500} />
  <div className="text-sm text-muted-foreground text-right">
    {value.length}/500
  </div>
</div>

Handle Validation

Provide clear validation feedback:

<Textarea
  className={error ? "border-destructive" : ""}
  aria-invalid={!!error}
  aria-describedby={error ? "error-message" : undefined}
/>
{error && (
  <p id="error-message" className="text-sm text-destructive">
    {error}
  </p>
)}

Styling

The Textarea component supports extensive customization:

// Custom height
<Textarea className="min-h-[100px]" />

// Code editor style
<Textarea className="font-mono bg-muted/30" />

// Rounded corners
<Textarea className="rounded-xl" />

// No resize
<Textarea className="resize-none" />

Common Patterns

Auto-save with Debouncing

import { useState, useEffect } from "react"
import { useDebounce } from "use-debounce"

function AutoSaveTextarea() {
  const [value, setValue] = useState("")
  const [debouncedValue] = useDebounce(value, 500)

  useEffect(() => {
    if (debouncedValue) {
      // Auto-save logic here
      console.log("Auto-saving:", debouncedValue)
    }
  }, [debouncedValue])

  return (
    <Textarea
      value={value}
      onChange={(e) => setValue(e.target.value)}
      placeholder="Your content will be auto-saved..."
    />
  )
}