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.
"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
Prop | Type | Default |
---|---|---|
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