Textarea Field
Multi-line text input component with optional tooltip. Built on shadcn InputGroup for enhanced UI.
npx shadcn@latest add https://shuip.plvo.dev/r/rhf-textarea-field.json
pnpm dlx shadcn@latest add https://shuip.plvo.dev/r/rhf-textarea-field.json
bun x shadcn@latest add https://shuip.plvo.dev/r/rhf-textarea-field.json
import { InfoIcon } from 'lucide-react';import type * as React from 'react';import type { FieldPath, FieldValues, UseFormRegisterReturn } from 'react-hook-form';import { FormControl, FormDescription, FormField, FormItem, FormLabel, FormMessage } from '@/components/ui/form';import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupTextarea } from '@/components/ui/input-group';import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';export interface TextareaFieldProps<T extends FieldValues> extends React.ComponentProps<typeof InputGroupTextarea> {register: UseFormRegisterReturn<FieldPath<T>>;label?: string;description?: string;tooltip?: React.ReactNode;}export function TextareaField<T extends FieldValues>({register,label,description,tooltip,...props}: TextareaFieldProps<T>) {return (<FormField{...register}render={({ field, fieldState }) => {return (<FormItem data-invalid={fieldState.invalid}>{label && <FormLabel>{label}</FormLabel>}<FormControl><InputGroup><InputGroupTextarea {...field} aria-invalid={fieldState.invalid} {...props} />{tooltip && (<InputGroupAddon align='block-end' className='justify-end'><Tooltip><TooltipTrigger asChild><InputGroupButton aria-label='Info' size='icon-xs'><InfoIcon /></InputGroupButton></TooltipTrigger><TooltipContent>{tooltip}</TooltipContent></Tooltip></InputGroupAddon>)}</InputGroup></FormControl><FormMessage className='text-xs text-left' />{description && <FormDescription className='text-xs'>{description}</FormDescription>}</FormItem>);}}/>);}
Loading...
Multi-line text inputs are essential for longer content like descriptions, comments, or messages. TextareaField wraps a textarea element with InputGroup to enable an optional tooltip button positioned at the bottom-right corner—useful for character limits or formatting guidelines without cluttering the label area.
The component accepts native textarea props like rows and maxLength directly. Common patterns include pairing maxLength with Zod validation for minimum length requirements or using the description prop to display character count guidance.
Built-in features
- Bottom-right tooltip positioned via InputGroup
align='block-end' - Native textarea props support (rows, maxLength, placeholder)
- Zod validation: Native integration with react-hook-form and Zod schemas
- Multi-line error display for longer validation messages
Usage patterns
const schema = z.object({
feedback: z.string().max(500, 'Maximum 500 characters'),
})
const form = useForm({
defaultValues: { feedback: '' },
resolver: zodResolver(schema),
})Basic textarea
<TextareaField
register={form.register('bio')}
label='Bio'
description='Tell us about yourself'
rows={4}
/>With character limit
<TextareaField
register={form.register('feedback')}
label='Feedback'
description='Share your thoughts (max 500 characters)'
rows={6}
maxLength={500}
placeholder='Your feedback...'
/>With tooltip
<TextareaField
register={form.register('notes')}
label='Notes'
tooltip='Supports plain text only. Markdown coming soon.'
rows={8}
/>Examples
Character Count
Loading...
'use client';import { zodResolver } from '@hookform/resolvers/zod';import { useForm } from 'react-hook-form';import { z } from 'zod';import { Form } from '@/components/ui/form';import { TextareaField } from '@/components/ui/shuip/react-hook-form/textarea-field';import { SubmitButton } from '@/components/ui/shuip/submit-button';const MAX_LENGTH = 280;const zodSchema = z.object({tweet: z.string().min(1, 'Tweet cannot be empty').max(MAX_LENGTH, `Tweet is too long (max ${MAX_LENGTH} characters)`),});export default function RhfTextareaFieldCharacterCountExample() {const form = useForm({defaultValues: { tweet: '' },resolver: zodResolver(zodSchema),});const tweet = form.watch('tweet');const remaining = MAX_LENGTH - tweet.length;const isNearLimit = remaining < 50;const isOverLimit = remaining < 0;async function onSubmit(values: z.infer<typeof zodSchema>) {try {alert(`Tweet: ${values.tweet}`);} catch (error) {console.error(error);}}return (<Form {...form}><form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4 w-full max-w-lg'><div className='space-y-2'><TextareaFieldregister={form.register('tweet')}label='Tweet'rows={4}maxLength={MAX_LENGTH}placeholder="What's happening?"/><div className='flex justify-between text-sm'><span className='text-muted-foreground'>{tweet.length} / {MAX_LENGTH} characters</span><spanclassName={`font-medium ${isOverLimit ? 'text-destructive' : isNearLimit ? 'text-orange-500' : 'text-muted-foreground'}`}>{remaining} remaining</span></div></div><SubmitButton>Post Tweet</SubmitButton></form></Form>);}
Default
Loading...
'use client';import { zodResolver } from '@hookform/resolvers/zod';import { useForm } from 'react-hook-form';import { z } from 'zod';import { Form } from '@/components/ui/form';import { TextareaField } from '@/components/ui/shuip/react-hook-form/textarea-field';import { SubmitButton } from '@/components/ui/shuip/submit-button';const zodSchema = z.object({bio: z.string().min(20, 'Bio must be at least 20 characters'),feedback: z.string().max(500, 'Maximum 500 characters'),});export default function RhfTextareaFieldExample() {const form = useForm({defaultValues: { bio: '', feedback: '' },resolver: zodResolver(zodSchema),});async function onSubmit(values: z.infer<typeof zodSchema>) {try {alert(`Bio: ${values.bio}\nFeedback: ${values.feedback}`);} catch (error) {console.error(error);}}return (<Form {...form}><form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'><TextareaFieldregister={form.register('bio')}label='Biography'description='Tell us about yourself'rows={4}placeholder='Software engineer passionate about...'/><TextareaFieldregister={form.register('feedback')}label='Feedback'description='Share your thoughts (max 500 characters)'rows={6}maxLength={500}placeholder='Your feedback...'/><SubmitButton>Submit</SubmitButton></form></Form>);}
Tooltip
Loading...
'use client';import { zodResolver } from '@hookform/resolvers/zod';import { useForm } from 'react-hook-form';import { z } from 'zod';import { Form } from '@/components/ui/form';import { TextareaField } from '@/components/ui/shuip/react-hook-form/textarea-field';import { SubmitButton } from '@/components/ui/shuip/submit-button';const zodSchema = z.object({notes: z.string().min(10, 'Notes must be at least 10 characters'),});export default function RhfTextareaFieldTooltipExample() {const form = useForm({defaultValues: { notes: '' },resolver: zodResolver(zodSchema),});async function onSubmit(values: z.infer<typeof zodSchema>) {try {alert(`Notes: ${values.notes}`);} catch (error) {console.error(error);}}return (<Form {...form}><form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'><TextareaFieldregister={form.register('notes')}label='Notes'description='Add any additional notes or comments'tooltip={<div className='space-y-1 text-sm'><p className='font-semibold'>Formatting tips:</p><ul className='list-disc list-inside space-y-0.5'><li>Keep it concise and clear</li><li>Use bullet points for lists</li><li>Mention important details first</li></ul></div>}rows={8}placeholder='Enter your notes here...'/><SubmitButton>Save Notes</SubmitButton></form></Form>);}
Props
Prop
Type