Input Field

Text input component integrated with React Hook Form. Supports tooltips and InputGroup integration for addons and buttons.

npx shadcn@latest add https://shuip.plvo.dev/r/rhf-input-field.json
pnpm dlx shadcn@latest add https://shuip.plvo.dev/r/rhf-input-field.json
bun x shadcn@latest add https://shuip.plvo.dev/r/rhf-input-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, InputGroupInput } from '@/components/ui/input-group';
import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';
export interface InputFieldProps<T extends FieldValues> extends React.ComponentProps<typeof InputGroupInput> {
register: UseFormRegisterReturn<FieldPath<T>>;
label?: string;
description?: string;
tooltip?: React.ReactNode;
}
export function InputField<T extends FieldValues>({
register,
label,
description,
tooltip,
...props
}: InputFieldProps<T>) {
return (
<FormField
{...register}
render={({ field, fieldState }) => {
return (
<FormItem data-invalid={fieldState.invalid}>
{label && <FormLabel>{label}</FormLabel>}
<FormControl>
<InputGroup>
<InputGroupInput {...field} type='text' aria-invalid={fieldState.invalid} {...props} />
{tooltip && (
<InputGroupAddon align='inline-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...

InputField is a text input component that encapsulates React Hook Form's field management with shadcn/ui's design system. It handles all the boilerplate of connecting form state to an input element: wiring event handlers, displaying errors, managing touched states, and rendering consistent UI.

This component is useful when you want to quickly add form inputs without manually setting up FormField render props, input bindings, and error display logic for every field.

Built-in features

  • Type-safe registration: Uses register from React Hook Form for validation
  • Tooltip integration: Optional InfoIcon button with tooltip content via tooltip prop
  • InputGroup ready: Built on shadcn InputGroup for seamless addon integration
  • Zod validation: Native integration with react-hook-form and Zod schemas
  • Full type inference: Field value types automatically inferred from form schema

Less boilerplate

React Hook Form's standard approach uses render props to access field state:

<FormField
  control={form.control}
  name="email"
  render={({ field }) => (
    <FormItem>
      <FormLabel>Email</FormLabel>
      <FormControl>
        <Input placeholder="your@email.com" {...field} />
      </FormControl>
      <FormDescription>
        Your email address
      </FormDescription>
      <FormMessage />
    </FormItem>
  )}
/>

With InputField, this reduces to a single declarative component:

<InputField
  register={form.register('email')}
  label="Email"
  description="Your email address"
  placeholder="your@email.com"
/>

Examples

Email

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 { InputField } from '@/components/ui/shuip/react-hook-form/input-field';
import { SubmitButton } from '@/components/ui/shuip/submit-button';
const zodSchema = z.object({
email: z.email({ message: 'Invalid email' }),
});
export default function InputFieldExample() {
const form = useForm({
defaultValues: { email: '' },
resolver: zodResolver(zodSchema),
});
async function onSubmit(values: z.infer<typeof zodSchema>) {
try {
alert(`Hello ${values.email}`);
} catch (error) {
console.error(error);
}
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'>
<InputField
register={form.register('email')}
type='email'
label='Email'
description='Your email'
placeholder='john@example.com'
/>
<SubmitButton>Check</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 { InputField } from '@/components/ui/shuip/react-hook-form/input-field';
import { SubmitButton } from '@/components/ui/shuip/submit-button';
const zodSchema = z.object({
name: z.string().nonempty({ message: 'Name is required' }),
});
export default function InputFieldExample() {
const form = useForm({
defaultValues: { name: '' },
resolver: zodResolver(zodSchema),
});
async function onSubmit(values: z.infer<typeof zodSchema>) {
try {
alert(`Hello ${values.name}`);
} catch (error) {
console.error(error);
}
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'>
<InputField register={form.register('name')} label='Name' description='Your name' placeholder='John' />
<SubmitButton>Check</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 { InputField } from '@/components/ui/shuip/react-hook-form/input-field';
import { SubmitButton } from '@/components/ui/shuip/submit-button';
const zodSchema = z.object({
name: z.string().nonempty({ message: 'Name is required' }),
});
export default function InputFieldExample() {
const form = useForm({
defaultValues: { name: '' },
resolver: zodResolver(zodSchema),
});
async function onSubmit(values: z.infer<typeof zodSchema>) {
try {
alert(`Hello ${values.name}`);
} catch (error) {
console.error(error);
}
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'>
<InputField
register={form.register('name')}
label='Name'
description='Your name'
placeholder='John'
tooltip='This is a tooltip where you can put some text'
/>
<SubmitButton>Check</SubmitButton>
</form>
</Form>
);
}

Validation

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 { InputField } from '@/components/ui/shuip/react-hook-form/input-field';
import { SubmitButton } from '@/components/ui/shuip/submit-button';
const zodSchema = z.object({
username: z
.string()
.min(3, 'Username must be at least 3 characters')
.regex(/^[a-zA-Z0-9_]+$/, 'Only letters, numbers, and underscores allowed'),
email: z.string().email('Invalid email format'),
age: z.coerce.number().min(18, 'Must be at least 18 years old').max(120, 'Age must be realistic'),
});
export default function RhfInputFieldValidationExample() {
const form = useForm({
defaultValues: { username: '', email: '', age: '' },
resolver: zodResolver(zodSchema),
});
async function onSubmit(values: z.infer<typeof zodSchema>) {
try {
alert(`User: ${values.username}\nEmail: ${values.email}\nAge: ${values.age}`);
} catch (error) {
console.error(error);
}
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'>
<InputField
register={form.register('username')}
label='Username'
description='Username will be used for your profile'
placeholder='Enter username'
/>
<InputField
register={form.register('email')}
type='email'
label='Email'
description='We will send confirmation to this email'
placeholder='your@email.com'
/>
<InputField
register={form.register('age')}
type='number'
label='Age'
description='You must be 18 or older to register'
placeholder='25'
/>
<SubmitButton>Register</SubmitButton>
</form>
</Form>
);
}

Props

Prop

Type

On this page