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

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

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 { 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'>
<CheckboxField
lens={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

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 { 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'>
<CheckboxField
lens={featuresLens.focus('notifications')}
label='Enable push notifications'
description='Receive real-time updates about your activity'
/>
<CheckboxField
lens={featuresLens.focus('analytics')}
label='Enable analytics tracking'
description='Help us improve by sharing usage data'
/>
<CheckboxField
lens={featuresLens.focus('darkMode')}
label='Enable dark mode'
description='Switch to a darker color scheme'
/>
<CheckboxField
lens={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

On this page