Number Field
Numeric input component integrated with React Hook Form via typed lens binding from @hookform/lenses. Writes back valueAsNumber, supports range sliders and tooltips.
npx shadcn@latest add https://shuip.plvo.dev/r/rhf-number-field.json
pnpm dlx shadcn@latest add https://shuip.plvo.dev/r/rhf-number-field.json
bun x shadcn@latest add https://shuip.plvo.dev/r/rhf-number-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 NumberFieldPropsextends Omit<React.ComponentProps<typeof InputGroupInput>, 'value' | 'onChange' | 'type'> {lens: Lens<number>;label?: string;description?: string;tooltip?: React.ReactNode;type?: 'number' | 'range';}export function NumberField({ lens, label, description, tooltip, type = 'number', ...props }: NumberFieldProps) {const { field, fieldState } = useController(lens.interop());const id = props.id ?? field.name;const value = field.value == null || Number.isNaN(field.value) ? '' : field.value;return (<Field className='gap-2' data-invalid={fieldState.invalid}>{label && <FieldLabel htmlFor={id}>{label}</FieldLabel>}<InputGroup><InputGroupInput{...field}{...props}id={id}type={type}value={value}onChange={(e) => field.onChange(e.target.valueAsNumber)}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...
NumberField binds a numeric form field (Lens<number>) to a type='number' or type='range' input. The input writes back e.target.valueAsNumber so the form state stays correctly typed as number. Empty input becomes NaN, which validators can check with Number.isNaN(value).
For text fields, use InputField instead.
Built-in features
- Typed lens binding:
lens.focus('quantity')requires a numeric form field — typing is enforced valueAsNumberwrite-back: form state staysnumber, notstring- Range slider support: pass
type='range'to render a slider - 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
import { useLens } from '@hookform/lenses';
import { useForm } from 'react-hook-form';
import { Form } from '@/components/ui/form';
import { NumberField } from '@/components/ui/shuip/react-hook-form/number-field';
const form = useForm<{ quantity: number; ratio: number }>({
defaultValues: { quantity: 1, ratio: 0.5 },
});
const lens = useLens({ control: form.control });
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<NumberField lens={lens.focus('quantity')} label='Quantity' />
<NumberField lens={lens.focus('ratio')} type='range' label='Ratio' min={0} max={1} step={0.1} />
</form>
</Form>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 { NumberField } from '@/components/ui/shuip/react-hook-form/number-field';import { SubmitButton } from '@/components/ui/shuip/submit-button';const zodSchema = z.object({quantity: z.number().min(1, 'Must be at least 1'),ratio: z.number().min(0).max(1),});type Values = z.infer<typeof zodSchema>;export default function RhfNumberFieldExample() {const form = useForm<Values>({defaultValues: { quantity: 1, ratio: 0.5 },resolver: zodResolver(zodSchema),});const lens = useLens({ control: form.control });async function onSubmit(values: Values) {try {alert(`Quantity: ${values.quantity}\nRatio: ${values.ratio}`);} catch (error) {console.error(error);}}return (<Form {...form}><form onSubmit={form.handleSubmit(onSubmit)} className='space-y-4'><NumberField lens={lens.focus('quantity')} label='Quantity' description='Integer quantity' placeholder='1' /><NumberFieldlens={lens.focus('ratio')}type='range'label='Ratio'description='Rendered as a range slider'min={0}max={1}step={0.1}/><SubmitButton>Submit</SubmitButton></form></Form>);}
Props
Prop
Type