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/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 w-full rounded-[var(--radius)] border border-[hsl(var(--hu-border))] bg-[hsl(var(--hu-input))] px-3 py-2 text-sm ring-offset-[hsl(var(--hu-background))] placeholder:text-[hsl(var(--hu-muted-foreground))] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[hsl(var(--hu-ring))] focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 transition-all resize-y",
  {
    variants: {
      variant: {
        default: "border-[hsl(var(--hu-border))]",
        destructive:
          "border-[hsl(var(--hu-destructive))] focus-visible:ring-[hsl(var(--hu-destructive))]",
        ghost:
          "border-transparent bg-[hsl(var(--hu-accent))] focus-visible:bg-[hsl(var(--hu-input))] focus-visible:border-[hsl(var(--hu-border))]",
      },
      size: {
        sm: "min-h-[60px] px-2 py-1 text-xs",
        default: "min-h-[80px] px-3 py-2",
        lg: "min-h-[120px] px-4 py-2",
        xl: "min-h-[160px] 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 = false,
      onClear,
      value,
      onChange,
      ...props
    },
    ref,
  ) => {
    const [internalValue, setInternalValue] = React.useState(
      props.defaultValue || "",
    );
    const textareaRef = React.useRef<HTMLTextAreaElement>(null);

    React.useImperativeHandle(ref, () => textareaRef.current!, []);

    const isControlled = value !== undefined;
    const textareaValue = isControlled ? value : internalValue;

    const showClearButton =
      clearable && textareaValue && String(textareaValue).length > 0;

    const handleClear = React.useCallback(() => {
      const syntheticEvent = {
        target: { value: "" },
        currentTarget: { value: "" },
      } as React.ChangeEvent<HTMLTextAreaElement>;

      if (!isControlled) {
        setInternalValue("");
      }

      if (onChange) {
        onChange(syntheticEvent);
      }

      if (onClear) {
        onClear();
      }

      if (textareaRef.current) {
        textareaRef.current.focus();
      }
    }, [isControlled, onChange, onClear]);

    const handleChange = React.useCallback(
      (e: React.ChangeEvent<HTMLTextAreaElement>) => {
        if (!isControlled) {
          setInternalValue(e.target.value);
        }

        if (onChange) {
          onChange(e);
        }
      },
      [isControlled, onChange],
    );

    // Determine variant - if error is true, use destructive variant
    const effectiveVariant = error ? "destructive" : variant;

    return (
      <div className="relative">
        <textarea
          className={cn(
            textareaVariants({ variant: effectiveVariant, size }),
            className,
          )}
          ref={textareaRef}
          value={textareaValue}
          onChange={handleChange}
          {...props}
        />

        {/* Clear button */}
        {showClearButton && (
          <div className="absolute top-2 right-2">
            <button
              type="button"
              onClick={handleClear}
              className="text-[hsl(var(--hu-muted-foreground))] hover:text-[hsl(var(--hu-foreground))] transition-colors [&_svg]:size-4 [&_svg]:shrink-0 p-1 rounded-md hover:bg-[hsl(var(--hu-accent))]"
              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-[hsl(var(--hu-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