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>
);
}
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.

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 tooltip prop
  • 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

Email

Loading...
'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'>
<InputField
lens={lens.focus('email')}
type='email'
label='Email'
description='Your email'
placeholder='john@example.com'
/>
<SubmitButton>Check</SubmitButton>
</form>
</Form>
);
}

Default

Loading...
'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

Loading...
'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'
tooltip='This is a tooltip where you can put some text'
/>
<SubmitButton>Check</SubmitButton>
</form>
</Form>
);
}

Validation

Loading...
'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'>
<InputField
lens={lens.focus('username')}
label='Username'
description='Username will be used for your profile'
placeholder='Enter username'
/>
<InputField
lens={lens.focus('email')}
type='email'
label='Email'
description='We will send confirmation to this email'
placeholder='your@email.com'
/>
<NumberField
lens={lens.focus('age')}
label='Age'
description='You must be 18 or older to register'
placeholder='25'
/>
<SubmitButton>Register</SubmitButton>
</form>
</Form>
);
}

Props

Prop

Type

On this page