Production-ready form components for React & Next.js.
Schema validation, accessible UI, nice error messages, loading states — all wired up and ready to use.
npm install formcraft
# or
pnpm add formcraft
# or
yarn add formcraftPeer dependencies:
react >=18,zod >=3(optional but recommended)
Forms in React apps are still painful:
- Setting up
react-hook-form+ Zod + error display every time - Rolling your own accessible components with ARIA attributes
- Handling loading states, password toggles, character counts
- Building drag-and-drop file uploads from scratch
- Multi-step forms with per-step validation
FormCraft gives you all of that in one import.
// app/login/page.tsx (Next.js App Router)"use client";import{Form,Input,SubmitButton,useFormCraft,loginSchema}from"formcraft";importtype{LoginFormValues}from"formcraft";exportdefaultfunctionLoginPage(){constform=useFormCraft<LoginFormValues>({schema: loginSchema,defaultValues: {email: "",password: ""},});asyncfunctiononSubmit(data: LoginFormValues){awaitsignIn(data);// your auth call}return(<Formform={form}onSubmit={onSubmit}className="fc-spaced"><Inputname="email"control={form.control}label="Email"type="email"required/><Inputname="password"control={form.control}label="Password"type="password"showPasswordTogglerequired/><SubmitButtonloading={form.formState.isSubmitting}loadingText="Signing in...">
Sign in
</SubmitButton></Form>);}Add the stylesheet once in your app/layout.tsx (or _app.tsx):
import"formcraft/styles";Tailwind CSS must be installed in your project. FormCraft uses Tailwind utility classes and dark mode via dark: variants.
Root form element. Provides form context; wraps react-hook-form's handleSubmit.
<Formform={form}onSubmit={handleSubmit}className="fc-spaced">{/* fields */}</Form>Add the fc-spaced class to get automatic vertical spacing between fields.
| Prop | Type | Description |
|---|---|---|
form | UseFormReturn | Form instance from useFormCraft |
onSubmit | (data) => void | Promise<void> | Called with validated data |
className | string | Additional CSS classes |
Covers: text, email, password, number, search, tel, url.
<Inputname="email"control={form.control}label="Email"type="email"placeholder="you@example.com"requiredleftIcon={<MailIcon/>}/><Inputname="password"control={form.control}label="Password"type="password"showPasswordToggle/>| Prop | Type | Default |
|---|---|---|
name | FieldPath<T> | — |
control | Control<T> | — |
label | string | — |
description | string | — |
leftIcon | ReactNode | — |
rightIcon | ReactNode | — |
loading | boolean | false |
showPasswordToggle | boolean | false |
<Textareaname="bio"control={form.control}label="Bio"showCountmaxLength={300}autoResize/>| Prop | Type | Default |
|---|---|---|
showCount | boolean | false |
maxLength | number | — |
autoResize | boolean | false |
<Selectname="country"control={form.control}label="Country"placeholder="Select a country"options={[{value: "us",label: "United States"},{value: "ph",label: "Philippines"},]}/><Checkboxname="acceptTerms"control={form.control}label="I agree to the Terms of Service"/><RadioGroupname="plan"control={form.control}label="Plan"options={[{value: "free",label: "Free",description: "Up to 3 projects"},{value: "pro",label: "Pro",description: "Unlimited projects"},]}/><Switchname="notifications"control={form.control}label="Email notifications"description="Receive weekly digest emails"/>Drag-and-drop with type/size validation.
<FileUploadname="avatar"control={form.control}label="Profile photo"accept="image/*"maxSize={5*1024*1024}// 5MB/>| Prop | Type | Default |
|---|---|---|
accept | string | — |
multiple | boolean | false |
maxSize | number (bytes) | — |
maxFiles | number | 10 |
Dynamic repeating fields.
<FieldArrayname="members"control={form.control}label="Team members"addLabel="Add member"minItems={1}maxItems={10}defaultItem={{name: "",email: ""}}>{(index)=>(<><Inputname={`members.${index}.name`}control={form.control}label="Name"/><Inputname={`members.${index}.email`}control={form.control}label="Email"/></>)}</FieldArray><FormActionsalign="right"><buttontype="button"onClick={onCancel}>Cancel</button><SubmitButtonloading={form.formState.isSubmitting}loadingText="Saving..."variant="primary"// "primary" | "secondary" | "destructive" | "ghost"size="md"// "sm" | "md" | "lg">
Save changes
</SubmitButton></FormActions>Wraps react-hook-form with Zod resolver.
constform=useFormCraft({schema: myZodSchema,defaultValues: {email: ""},mode: "onBlur",// defaultreValidateMode: "onChange",});const{ step, totalSteps, next, back, goTo, isFirst, isLast, progress }=useMultiStepForm({steps: 3});Per-step validation:
asyncfunctionhandleNext(){constvalid=awaitform.trigger(["firstName","lastName"]);if(valid)next();}conststrength=usePasswordStrength(form.watch("password"));// { score: 3, label: "Good", color: "text-blue-500", percentage: 60 }<divstyle={{width: `${strength.percentage}%`}}className={strengthBarColor}/><spanclassName={strength.color}>{strength.label}</span>useFormPersist(form,{key: "signup-form",storage: "sessionStorage",excludeFields: ["password"],});const{ isSaving, lastSaved }=useFormAutoSave(form,{onSave: async(data)=>awaitapi.saveDraft(data),debounce: 1500,});Import pre-built Zod schemas or build your own with the helpers.
import{emailSchema,passwordSchema,phoneSchema,urlSchema,requiredString,mustBeChecked,loginSchema,registerSchema,contactSchema,}from"formcraft";import{z}from"zod";import{emailSchema,requiredString,mustBeChecked}from"formcraft";constcheckoutSchema=z.object({email: emailSchema,address: requiredString("Address"),city: requiredString("City"),acceptTerms: mustBeChecked("Please accept terms"),});Every FormCraft component is built with accessibility first:
- All inputs have associated
<label>viahtmlFor/id(auto-generated) - Error messages use
role="alert"+aria-live="polite" - Inputs set
aria-invalidwhen there's an error aria-describedbylinks inputs to their descriptions and errorsRadioGroupusesrole="radiogroup"Switchusesrole="switch"+aria-checkedFileUploadzone is keyboard-navigable (Enter/Spaceto open)- All interactive elements have
:focus-visiblestyles - Shake animation is
prefers-reduced-motionsafe (CSS only)
All components accept a generic TFieldValues parameter. Because name is typed as FieldPath<TFieldValues>, you get full autocomplete:
// ✅ TypeScript error if "emaill" doesn't exist in your schema<Inputname="emaill"control={form.control}/>FormCraft supports Tailwind's dark: class strategy. Ensure your Tailwind config has:
// tailwind.config.jsmodule.exports={darkMode: "class",// or "media"content: ["./node_modules/formcraft/dist/**/*.{js,mjs}"],};DatePickercomponentComboBox/ searchable selectPinInput(OTP)FormSkeletonloading placeholder- React Native support
PRs and issues are welcome! See CONTRIBUTING.md.
git clone https://github.com/formcraft/formcraft
pnpm install
pnpm dev # watch mode
pnpm test# run testsMIT © FormCraft (JohnDev19)