Input Field
Text input component integrated with React Hook Form via typed lens binding from @hookform/lenses. Supports tooltips and InputGroup integration.
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
'use client';import type { Lens } from '@hookform/lenses';import { InfoIcon } from 'lucide-react';import type * as React from 'react';import { useController } from 'react-hook-form';import { Field, FieldDescription, FieldError, FieldLabel } from '@/components/ui/field';import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput } from '@/components/ui/input-group';import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip';export interface InputFieldProps extends Omit<React.ComponentProps<typeof InputGroupInput>, 'value' | 'onChange'> {lens: Lens<string>;label?: string;description?: string;tooltip?: React.ReactNode;}export function InputField({ lens, label, description, tooltip, ...props }: InputFieldProps) {const { field, fieldState } = useController(lens.interop());return (<Field className='gap-2' data-invalid={fieldState.invalid}>{label && <FieldLabel htmlFor={field.name}>{label}</FieldLabel>}<InputGroup><InputGroupInput{...field}{...props}id={field.name}value={field.value ?? ''}aria-invalid={fieldState.invalid}/>{tooltip && (<InputGroupAddon align='inline-end'><Tooltip><TooltipTrigger asChild><InputGroupButton aria-label='Info' size='icon-xs'><InfoIcon /></InputGroupButton></TooltipTrigger><TooltipContent>{tooltip}</TooltipContent></Tooltip></InputGroupAddon>)}</InputGroup>{fieldState.invalid && <FieldError className='text-xs text-left' errors={[fieldState.error]} />}{description && <FieldDescription className='text-xs'>{description}</FieldDescription>}</Field>);}
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.
The field binds to the form via a typed lens from @hookform/lenses — no call-site generic, just lens.focus('fieldName') with full autocomplete from your form's value type.
For numeric inputs, use NumberField instead.
Built-in features
- Typed lens binding:
lens.focus('name')autocompletes from your form's value type — no<MyForm>generic at the call site - Tooltip integration: optional InfoIcon button with tooltip content via
tooltipprop - InputGroup ready: built on shadcn InputGroup for seamless addon integration
- Zod validation: native integration with react-hook-form and Zod via resolver
Setup
Field components bind via @hookform/lenses. Create a lens once per form:
import { useLens } from '@hookform/lenses';
import { useForm } from 'react-hook-form';
import { Form } from '@/components/ui/form';
import { InputField } from '@/components/ui/shuip/react-hook-form/input-field';
const form = useForm<MyForm>({ defaultValues: { email: '' } });
const lens = useLens({ control: form.control });
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<InputField lens={lens.focus('email')} label='Email' />
</form>
</Form>The <Form> wrapper is required — it provides React Hook Form's FormProvider context.
Less boilerplate
React Hook Form's standard approach uses render props to access field state:
<Controller
control={form.control}
name="email"
render={({ field, fieldState }) => (
<Field data-invalid={fieldState.invalid}>
<FieldLabel htmlFor={field.name}>Email</FieldLabel>
<Input {...field} id={field.name} aria-invalid={fieldState.invalid} placeholder="your@email.com" />
<FieldDescription>Your email address</FieldDescription>
{fieldState.invalid && <FieldError errors={[fieldState.error]} />}
</Field>
)}
/>With InputField, this reduces to a single declarative component:
<InputField
lens={lens.focus('email')}
label="Email"
description="Your email address"
placeholder="your@email.com"
/>Examples
'use client';import { useLens } from '@hookform/lenses';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' }),});type Values = z.infer<typeof zodSchema>;export default function InputFieldExample() {const form = useForm<Values>({defaultValues: { email: '' },resolver: zodResolver(zodSchema),});const lens = useLens({ control: form.control });async function onSubmit(values: Values) {try {alert(`Hello ${values.email}`);} catch (error) {console.error(error);}}return (<Form {...form}><form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'><InputFieldlens={lens.focus('email')}type='email'label='Email'description='Your email'placeholder='john@example.com'/><SubmitButton>Check</SubmitButton></form></Form>);}
Default
'use client';import { useLens } from '@hookform/lenses';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' }),});type Values = z.infer<typeof zodSchema>;export default function InputFieldExample() {const form = useForm<Values>({defaultValues: { name: '' },resolver: zodResolver(zodSchema),});const lens = useLens({ control: form.control });async function onSubmit(values: Values) {try {alert(`Hello ${values.name}`);} catch (error) {console.error(error);}}return (<Form {...form}><form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'><InputField lens={lens.focus('name')} label='Name' description='Your name' placeholder='John' /><SubmitButton>Check</SubmitButton></form></Form>);}
Tooltip
'use client';import { useLens } from '@hookform/lenses';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' }),});type Values = z.infer<typeof zodSchema>;export default function InputFieldExample() {const form = useForm<Values>({defaultValues: { name: '' },resolver: zodResolver(zodSchema),});const lens = useLens({ control: form.control });async function onSubmit(values: Values) {try {alert(`Hello ${values.name}`);} catch (error) {console.error(error);}}return (<Form {...form}><form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'><InputFieldlens={lens.focus('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
'use client';import { useLens } from '@hookform/lenses';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 { NumberField } from '@/components/ui/shuip/react-hook-form/number-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.number().min(18, 'Must be at least 18 years old').max(120, 'Age must be realistic'),});type Values = z.infer<typeof zodSchema>;export default function RhfInputFieldValidationExample() {const form = useForm<Values>({defaultValues: { username: '', email: '', age: 0 },resolver: zodResolver(zodSchema),});const lens = useLens({ control: form.control });async function onSubmit(values: Values) {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'><InputFieldlens={lens.focus('username')}label='Username'description='Username will be used for your profile'placeholder='Enter username'/><InputFieldlens={lens.focus('email')}type='email'label='Email'description='We will send confirmation to this email'placeholder='your@email.com'/><NumberFieldlens={lens.focus('age')}label='Age'description='You must be 18 or older to register'placeholder='25'/><SubmitButton>Register</SubmitButton></form></Form>);}
Props
Prop
Type