FluxUI Pro is live - modern UI, powerful animations, zero hassle.
Components
Textarea

Textarea

Ultra-modern textarea component with auto-resize, character counting, resizable handles, and enhanced user experience features.

Basic Features

Basic Textarea

Characters: 0

With Description

Please share your thoughts about our service

Error State

Please enter some text

Disabled State

Auto-Resize Feature

This textarea will automatically grow as you type more content

0/1000

Try it: Type multiple lines of text above. The textarea will automatically adjust its height to fit the content.

Resizable Feature

You can manually resize this textarea by dragging the handle

Try it: Look for the resize handle (⋮) in the bottom-right corner and drag to resize the textarea.

Character Limits & Validation

Character Count

0/200

Limited Text (10-100 chars)

0/100

Variants

Default Variant

Filled Variant

Outlined Variant

Sizes

Interactive Form Demo

Contact Form

Tell us how we can help you

0/500

Keyboard Navigation

Try these keyboard shortcuts:

  • Tab: Move focus between textareas
  • Enter: Create new lines
  • Ctrl+A: Select all text
  • Ctrl+Z: Undo last action

Installation

1

Install the packages

npm i motion clsx tailwind-merge lucide-react
2

Add util file

lib/util.ts
import { ClassValue, clsx } from "clsx"; import { twMerge } from "tailwind-merge"; export function cn(...inputs: ClassValue[]) { return twMerge(clsx(inputs)); }
3

Copy and paste the following code into your project

textarea.tsx
"use client"; import { motion, AnimatePresence } from "motion/react"; import React, { useState, useRef, useEffect, useId } from "react"; import { cn } from "@/lib/utils"; import { AlertCircle, GripVertical } from "lucide-react"; interface TextareaProps extends Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'size'> { label?: string; description?: string; error?: string; maxLength?: number; minLength?: number; autoResize?: boolean; resizable?: boolean; showCount?: boolean; size?: "sm" | "md" | "lg"; variant?: "default" | "filled" | "outlined"; } const Textarea: React.FC<TextareaProps> = ({ label, description, error, maxLength, minLength, autoResize = false, resizable = false, showCount = false, size = "md", variant = "default", className, value, onChange, rows = 3, ...props }) => { const [isFocused, setIsFocused] = useState(false); const [currentValue, setCurrentValue] = useState(value || ""); const textareaRef = useRef<HTMLTextAreaElement>(null); const textareaId = useId(); const hasError = !!error; const sizeClasses = { sm: "px-3 py-2 text-sm", md: "px-4 py-3 text-base", lg: "px-4 py-4 text-lg", }; const variantClasses = { default: "border border-gray-300 bg-white focus:border-blue-500 focus:ring-blue-500/20 dark:border-gray-600 dark:bg-gray-800 dark:focus:border-blue-400", filled: "border-transparent bg-gray-100 focus:bg-white focus:border-blue-500 focus:ring-blue-500/20 dark:bg-gray-800 dark:focus:bg-gray-900", outlined: "border-2 border-gray-300 bg-transparent focus:border-blue-500 focus:ring-blue-500/20 dark:border-gray-600", }; const errorClasses = "border-red-500 focus:border-red-500 focus:ring-red-500/20"; // Auto-resize functionality useEffect(() => { if (autoResize && textareaRef.current) { const textarea = textareaRef.current; textarea.style.height = "auto"; textarea.style.height = `${textarea.scrollHeight}px`; } }, [currentValue, autoResize]); // Handle value changes const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => { const newValue = e.target.value; setCurrentValue(newValue); if (onChange) { onChange(e); } }; // Handle focus/blur const handleFocus = (e: React.FocusEvent<HTMLTextAreaElement>) => { setIsFocused(true); props.onFocus?.(e); }; const handleBlur = (e: React.FocusEvent<HTMLTextAreaElement>) => { setIsFocused(false); props.onBlur?.(e); }; // Character count const characterCount = currentValue.length; const isOverLimit = maxLength && characterCount > maxLength; const isUnderLimit = minLength && characterCount < minLength && characterCount > 0; return ( <div className={cn("relative", className)}> {/* Label */} {label && ( <motion.label htmlFor={textareaId} className={cn( "block text-sm font-medium mb-2 transition-colors duration-200", hasError ? "text-red-600 dark:text-red-400" : "text-gray-700 dark:text-gray-300" )} animate={{ scale: isFocused || currentValue ? 1 : 1, y: isFocused || currentValue ? 0 : 0, }} transition={{ duration: 0.2 }} > {label} {props.required && <span className="text-red-500 ml-1">*</span>} </motion.label> )} {/* Textarea Container */} <div className="relative"> <motion.textarea ref={textareaRef} id={textareaId} value={currentValue} onChange={handleChange} onFocus={handleFocus} onBlur={handleBlur} rows={rows} maxLength={maxLength} minLength={minLength} className={cn( "w-full rounded-lg transition-all duration-200 bg-transparent border outline-none focus:ring-2 disabled:cursor-not-allowed disabled:opacity-50 resize-none", sizeClasses[size], variantClasses[variant], hasError && errorClasses, isFocused && "ring-2 ring-blue-500/20", resizable && "resize", autoResize && "overflow-hidden", className )} animate={{ scale: isFocused ? 1.01 : 1, boxShadow: isFocused ? "0 0 0 3px rgba(59, 130, 246, 0.1)" : "0 0 0 0px rgba(0, 0, 0, 0)", }} transition={{ duration: 0.2 }} {...props} /> {/* Resize Handle */} {resizable && ( <motion.div className="absolute bottom-2 right-2 cursor-se-resize opacity-50 hover:opacity-100 transition-opacity duration-200" whileHover={{ scale: 1.1 }} whileTap={{ scale: 0.9 }} > <GripVertical className="w-4 h-4 text-gray-400" /> </motion.div> )} {/* Character Count */} {showCount && maxLength && ( <motion.div className={cn( "absolute bottom-2 right-2 text-xs transition-colors duration-200", isOverLimit ? "text-red-500" : characterCount > maxLength * 0.8 ? "text-yellow-500" : "text-gray-400" )} initial={{ opacity: 0 }} animate={{ opacity: 1 }} transition={{ duration: 0.2 }} > {characterCount}/{maxLength} </motion.div> )} {/* Focus Ring Animation */} <AnimatePresence> {isFocused && ( <motion.div className="absolute inset-0 rounded-lg border-2 border-blue-500 pointer-events-none" initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.95 }} transition={{ duration: 0.2 }} /> )} </AnimatePresence> </div> {/* Description */} {description && ( <motion.p className={cn( "mt-2 text-sm transition-colors duration-200", disabled ? "text-gray-400 dark:text-gray-500" : "text-gray-600 dark:text-gray-400" )} animate={{ x: isFocused ? 2 : 0, }} transition={{ duration: 0.2, delay: 0.05 }} > {description} </motion.p> )} {/* Error Message */} <AnimatePresence> {hasError && ( <motion.div initial={{ opacity: 0, y: -10, height: 0 }} animate={{ opacity: 1, y: 0, height: "auto" }} exit={{ opacity: 0, y: -10, height: 0 }} transition={{ duration: 0.2 }} className="mt-2 text-sm text-red-600 dark:text-red-400 flex items-center gap-1" > <AlertCircle className="w-4 h-4 flex-shrink-0" /> {error} </motion.div> )} </AnimatePresence> {/* Validation Messages */} <AnimatePresence> {isUnderLimit && minLength && ( <motion.div initial={{ opacity: 0, y: -10, height: 0 }} animate={{ opacity: 1, y: 0, height: "auto" }} exit={{ opacity: 0, y: -10, height: 0 }} transition={{ duration: 0.2 }} className="mt-2 text-sm text-yellow-600 dark:text-yellow-400 flex items-center gap-1" > <AlertCircle className="w-4 h-4 flex-shrink-0" /> Minimum {minLength} characters required </motion.div> )} </AnimatePresence> </div> ); }; export default Textarea;
4

Update the import paths to match your project setup

Props

PropTypeDefaultDescription
valuestringundefinedThe value of the textarea.
onChange(value: string) => void() => {}Callback function when textarea value changes.
placeholderstringundefinedPlaceholder text when textarea is empty.
labelstringundefinedLabel text for the textarea.
descriptionstringundefinedDescription text below the label.
disabledbooleanfalseDisable the textarea interaction.
requiredbooleanfalseMark textarea as required.
errorstringundefinedError message to display.
maxLengthnumberundefinedMaximum number of characters allowed.
minLengthnumberundefinedMinimum number of characters required.
rowsnumber3Number of visible text lines.
autoResizebooleanfalseAutomatically resize textarea based on content.
resizablebooleanfalseAllow manual resizing with drag handles.
showCountbooleanfalseShow character count.
size'sm' | 'md' | 'lg''md'Size of the textarea.
variant'default' | 'filled' | 'outlined''default'Visual style variant.