Build websites 10x faster with HextaUI Blocks — Learn more
UI/UI

Textarea

Displays a multi-line text input field with enhanced features like clearable functionality, auto-resizing, and Zod validation support.

<Textarea placeholder="Enter your message here..." />

Installation

Install following dependencies:

npm install class-variance-authority
pnpm add class-variance-authority
yarn add class-variance-authority
bun add class-variance-authority

Copy and paste the following code into your project.

components/ui/textarea.tsx
"use client";

import * as React from "react";
import { cva, type VariantProps } from "class-variance-authority";
import { cn } from "@/lib/utils";
import { X } from "lucide-react";

const textareaVariants = cva(
  "flex min-h-[60px] w-full rounded-ele border border-border bg-input px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 resize-vertical transition-colors scrollbar-thin scrollbar-track-transparent scrollbar-thumb-border hover:scrollbar-thumb-muted-foreground scrollbar-corner-transparent shadow-sm/2",
  {
    variants: {
      variant: {
        default: "border-border",
        destructive:
          "border-destructive focus-visible:ring-destructive",
        ghost:
          "border-transparent bg-accent focus-visible:bg-input focus-visible:border-border",
      },
      size: {
        default: "min-h-[80px] px-3 py-2",
        sm: "min-h-[60px] px-3 py-2 text-xs",
        lg: "min-h-[100px] px-4 py-2",
        xl: "min-h-[120px] px-6 py-3 text-base",
        fixed: "h-[80px] px-3 py-2 resize-none",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
);

export interface TextareaProps
  extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, "size">,
    VariantProps<typeof textareaVariants> {
  error?: boolean;
  clearable?: boolean;
  onClear?: () => void;
}

const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
  (
    { className, variant, size, error, clearable, onClear, value, ...props },
    ref
  ) => {
    const [internalValue, setInternalValue] = React.useState(
      props.defaultValue || ""
    );
    const textareaRef = React.useRef<HTMLTextAreaElement>(null);

    // Combine external ref with internal ref
    React.useImperativeHandle(ref, () => textareaRef.current!);

    const textareaVariant = error ? "destructive" : variant;

    // Determine if this is a controlled component
    const isControlled = value !== undefined;
    const textareaValue = isControlled ? value : internalValue;
    const showClearButton =
      clearable && textareaValue && String(textareaValue).length > 0;

    const handleTextareaChange = (
      e: React.ChangeEvent<HTMLTextAreaElement>
    ) => {
      if (!isControlled) {
        setInternalValue(e.target.value);
      }
      props.onChange?.(e);
    };

    const handleClear = () => {
      // Clear the internal state for uncontrolled inputs
      if (!isControlled) {
        setInternalValue("");
      }

      // Call the onClear callback if provided
      onClear?.();

      // Create a synthetic event to trigger onChange with empty value
      if (textareaRef.current) {
        const textarea = textareaRef.current;

        // Set the textarea's value directly
        textarea.value = "";

        // Create a synthetic React ChangeEvent
        const syntheticEvent = {
          target: textarea,
          currentTarget: textarea,
          nativeEvent: new Event("input", { bubbles: true }),
          isDefaultPrevented: () => false,
          isPropagationStopped: () => false,
          persist: () => {},
          preventDefault: () => {},
          stopPropagation: () => {},
          bubbles: true,
          cancelable: true,
          defaultPrevented: false,
          eventPhase: 0,
          isTrusted: true,
          timeStamp: Date.now(),
          type: "change",
        } as React.ChangeEvent<HTMLTextAreaElement>;

        // Trigger the onChange handler
        props.onChange?.(syntheticEvent);

        // Focus the textarea after clearing
        textarea.focus();
      }
    };

    return (
      <div className="relative w-full">
        <textarea
          className={cn(
            textareaVariants({ variant: textareaVariant, size, className }),
            showClearButton && "pr-10"
          )}
          style={{
            scrollbarWidth: "thin",
            scrollbarColor: "hsl(var(--hu-border)) transparent",
          }}
          ref={textareaRef}
          {...(isControlled
            ? { value: textareaValue }
            : { defaultValue: props.defaultValue })}
          onChange={handleTextareaChange}
          {...(({ defaultValue, ...rest }) => rest)(props)}
        />
        {/* Clear button */}
        {showClearButton && (
          <div className="absolute right-3 top-3 flex items-center gap-1 z-10">
            <button
              type="button"
              onClick={handleClear}
              className="text-muted-foreground hover:text-foreground transition-colors [&_svg]:size-4 [&_svg]:shrink-0"
              tabIndex={-1}
            >
              <X />
            </button>
          </div>
        )}
      </div>
    );
  }
);

Textarea.displayName = "Textarea";

export { Textarea, textareaVariants };
npx hextaui@latest add textarea
pnpm dlx hextaui@latest add textarea
yarn dlx hextaui@latest add textarea
bun x hextaui@latest add textarea

Usage

import { Textarea } from "@/components/ui/Textarea";
<div className="grid w-full max-w-sm items-center gap-1.5">
  <Textarea placeholder="Enter your message..." />
</div>

Examples

Basic Textarea

<div className="w-full max-w-sm">
  <Textarea placeholder="Enter your message here..." />
</div>

With Label

import { Label } from "@/components/ui/label";

<div className="w-full max-w-sm">
  <div className="grid w-full items-center gap-1.5">
    <Label htmlFor="message">Your message</Label>
    <Textarea id="message" placeholder="Type your message here..." />
  </div>
</div>;

Sizes

Small
Default
Large
XL
Fixed
<div className="flex flex-col gap-3 w-full max-w-sm">
  <Textarea placeholder="Small textarea" size="sm" />
  <Textarea placeholder="Default textarea" />
  <Textarea placeholder="Large textarea" size="lg" />
  <Textarea placeholder="Extra large textarea" size="xl" />
  <Textarea placeholder="Fixed height textarea (no resize)" size="fixed" />
</div>

Variants

<div className="flex flex-col gap-3 w-full max-w-sm">
  <Textarea placeholder="Default textarea" />
  <Textarea placeholder="Ghost textarea" variant="ghost" />
  <Textarea placeholder="Error textarea" error />
</div>

Clearable Textarea

<div className="flex flex-col gap-3 w-full max-w-sm">
  <Textarea
    placeholder="Clearable textarea"
    clearable
    defaultValue="Clear me!"
  />
  <Textarea
    placeholder="Another clearable textarea"
    clearable
    defaultValue="This content can be cleared with the X button"
  />
</div>

Disabled State

<div className="w-full max-w-sm">
  <Textarea
    placeholder="This textarea is disabled"
    disabled
    defaultValue="You cannot edit this content"
  />
</div>

With Error

This field is required and must be at least 10 characters.

import { Label } from "@/components/ui/label";

<div className="w-full max-w-sm">
  <div className="grid w-full items-center gap-1.5">
    <Label htmlFor="error-message" required>
      Message
    </Label>
    <Textarea id="error-message" placeholder="Enter your message..." error />
    <p className="text-xs text-destructive">
      This field is required.
    </p>
  </div>
</div>;

Form Examples with Zod Validation

Contact Form

"use client";

import { useState } from "react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/Textarea";

function ContactForm() {
  const [formData, setFormData] = useState({
    name: "",
    email: "",
    message: "",
  });

  const handleSubmit = (e: React.FormEvent) => {
    e.preventDefault();
    console.log("Form submitted:", formData);
  };

  const handleChange =
    (field: string) =>
    (e: React.ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => {
      setFormData((prev) => ({ ...prev, [field]: e.target.value }));
    };

  return (
    <div className="w-full max-w-md mx-auto">
      <form onSubmit={handleSubmit} className="space-y-4">
        <div className="grid w-full items-center gap-1.5">
          <Label htmlFor="contact-name" required>
            Name
          </Label>
          <Input
            id="contact-name"
            type="text"
            placeholder="Enter your name"
            value={formData.name}
            onChange={handleChange("name")}
            clearable
          />
        </div>
        <div className="grid w-full items-center gap-1.5">
          <Label htmlFor="contact-email" required>
            Email
          </Label>
          <Input
            id="contact-email"
            type="email"
            placeholder="Enter your email"
            value={formData.email}
            onChange={handleChange("email")}
            clearable
          />
        </div>
        <div className="grid w-full items-center gap-1.5">
          <Label htmlFor="contact-message" required>
            Message
          </Label>
          <Textarea
            id="contact-message"
            placeholder="Enter your message..."
            value={formData.message}
            onChange={handleChange("message")}
            clearable
            size="lg"
          />
        </div>
        <Button type="submit" className="w-full">
          Send Message
        </Button>
      </form>
    </div>
  );
}

Form Validation with Zod

The Textarea component works excellently with Zod for type-safe form validation:

import { z } from "zod";

const schema = z.object({
  message: z.string().min(10, "Message must be at least 10 characters"),
  email: z.string().email(),
});

// Use with error state for visual feedback
<Textarea error={!!errors.message} onChange={handleChange} />;

Props

PropTypeDefault
onBlur?
(event: React.FocusEvent<HTMLTextAreaElement>) => void
undefined
onFocus?
(event: React.FocusEvent<HTMLTextAreaElement>) => void
undefined
onChange?
(event: React.ChangeEvent<HTMLTextAreaElement>) => void
undefined
defaultValue?
string
undefined
value?
string
undefined
readOnly?
boolean
false
required?
boolean
false
minLength?
number
undefined
maxLength?
number
undefined
cols?
number
undefined
rows?
number
undefined
disabled?
boolean
false
placeholder?
string
undefined
className?
string
undefined
onClear?
() => void
undefined
clearable?
boolean
false
error?
boolean
false
size?
"sm" | "default" | "lg" | "xl" | "fixed"
"default"
variant?
"default" | "destructive" | "ghost"
"default"
Edit on GitHub

Last updated on