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 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
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