Checkbox Field
Checkbox component integrated with React Hook Form via typed lens binding from @hookform/lenses. Supports inline clickable labels and Zod refinements for terms acceptance.
npx shadcn@latest add https://shuip.plvo.dev/r/rhf-checkbox-field.json
pnpm dlx shadcn@latest add https://shuip.plvo.dev/r/rhf-checkbox-field.json
bun x shadcn@latest add https://shuip.plvo.dev/r/rhf-checkbox-field.json
'use client';import type { Lens } from '@hookform/lenses';import type * as React from 'react';import { useController } from 'react-hook-form';import { Checkbox } from '@/components/ui/checkbox';import { Field, FieldDescription, FieldError, FieldLabel } from '@/components/ui/field';export interface CheckboxFieldProps extends Omit<React.ComponentProps<typeof Checkbox>, 'checked' | 'onCheckedChange'> {lens: Lens<boolean>;label: string;description?: string;}export function CheckboxField({ lens, label, description, ...props }: CheckboxFieldProps) {const { field, fieldState } = useController(lens.interop());const id = props.id ?? field.name;return (<Field className='gap-2' data-invalid={fieldState.invalid}><div className='flex items-center gap-2'><Checkbox{...props}id={id}name={field.name}checked={field.value ?? false}onCheckedChange={(checked) => field.onChange(checked === true)}onBlur={field.onBlur}aria-invalid={fieldState.invalid}/><FieldLabel htmlFor={id} className='text-sm cursor-pointer'>{label}</FieldLabel></div>{fieldState.invalid && <FieldError className='text-xs text-left' errors={[fieldState.error]} />}{description && <FieldDescription className='text-xs'>{description}</FieldDescription>}</Field>);}
CheckboxField is a boolean input component that encapsulates React Hook Form's field management with shadcn/ui's design system. It combines Radix UI's Checkbox with an inline clickable label, making the entire label area interactive — not just the checkbox itself.
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.
Built-in features
- Typed lens binding:
lens.focus('agree')autocompletes from your form's value type — no<MyForm>generic at the call site - Clickable inline label: positioned next to the checkbox for a larger hit target
- Boolean field type: bound via
checked/onCheckedChange— no manual event wiring - Required validation: pair with Zod's
.refine()for terms acceptance flows - 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 { CheckboxField } from '@/components/ui/shuip/react-hook-form/checkbox-field';
const form = useForm<MyForm>({ defaultValues: { agree: false } });
const lens = useLens({ control: form.control });
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<CheckboxField lens={lens.focus('agree')} label='I agree' />
</form>
</Form>The <Form> wrapper is required — it provides shadcn's FormProvider which FormLabel and FormMessage use internally.
Less boilerplate
React Hook Form's standard approach uses render props to access field state:
<FormField
control={form.control}
name="agree"
render={({ field }) => (
<FormItem>
<FormControl>
<Checkbox checked={field.value} onCheckedChange={field.onChange} />
</FormControl>
<FormLabel>I accept the terms and conditions</FormLabel>
<FormMessage />
</FormItem>
)}
/>With CheckboxField, this reduces to a single declarative component:
<CheckboxField
lens={lens.focus('agree')}
label='I accept the terms and conditions'
description='Read our terms before proceeding'
/>Examples
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 { CheckboxField } from '@/components/ui/shuip/react-hook-form/checkbox-field';import { SubmitButton } from '@/components/ui/shuip/submit-button';const zodSchema = z.object({checkbox: z.boolean().refine((val) => val === true, {message: 'Accept your destiny!',}),});type Values = z.infer<typeof zodSchema>;export default function CheckboxFieldExample() {const form = useForm<Values>({defaultValues: { checkbox: false },resolver: zodResolver(zodSchema),});const lens = useLens({ control: form.control });async function onSubmit(values: Values) {try {alert(`Checkbox: ${values.checkbox}`);} catch (error) {console.error(error);}}return (<Form {...form}><form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'><CheckboxFieldlens={lens.focus('checkbox')}label='Accept terms and conditions'description='To continue, you must accept the terms and conditions because tbh it says so'/><SubmitButton>Submit</SubmitButton></form></Form>);}
Group
'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 { CheckboxField } from '@/components/ui/shuip/react-hook-form/checkbox-field';import { SubmitButton } from '@/components/ui/shuip/submit-button';const zodSchema = z.object({features: z.object({notifications: z.boolean(),analytics: z.boolean(),darkMode: z.boolean(),apiAccess: z.boolean(),}),});type Values = z.infer<typeof zodSchema>;export default function CheckboxFieldGroupExample() {const form = useForm<Values>({defaultValues: { features: { notifications: false, analytics: false, darkMode: false, apiAccess: false } },resolver: zodResolver(zodSchema),});const lens = useLens({ control: form.control });const featuresLens = lens.focus('features');async function onSubmit(values: Values) {try {alert(`Features: ${JSON.stringify(values.features, null, 2)}`);} catch (error) {console.error(error);}}return (<Form {...form}><form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'><div className='space-y-3'><h3 className='font-semibold'>Features</h3><p className='text-sm text-muted-foreground'>Select the features you want to enable</p><div className='space-y-3'><CheckboxFieldlens={featuresLens.focus('notifications')}label='Enable push notifications'description='Receive real-time updates about your activity'/><CheckboxFieldlens={featuresLens.focus('analytics')}label='Enable analytics tracking'description='Help us improve by sharing usage data'/><CheckboxFieldlens={featuresLens.focus('darkMode')}label='Enable dark mode'description='Switch to a darker color scheme'/><CheckboxFieldlens={featuresLens.focus('apiAccess')}label='Enable API access'description='Get programmatic access to your data'/></div></div><SubmitButton>Save Preferences</SubmitButton></form></Form>);}
Required checkbox
For terms acceptance, validate that the value is true with Zod's .refine():
const schema = z.object({
terms: z.boolean().refine((val) => val === true, {
message: 'You must accept the terms and conditions',
}),
});
const form = useForm<z.infer<typeof schema>>({
defaultValues: { terms: false },
resolver: zodResolver(schema),
});
const lens = useLens({ control: form.control });
<CheckboxField
lens={lens.focus('terms')}
label='I accept the terms and conditions'
description='Read our terms before proceeding'
/>Props
Prop
Type