Skip to content

Repository files navigation

FormCraft

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 formcraft

Peer dependencies:react >=18, zod >=3 (optional but recommended)


Why FormCraft?

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.


Quick Start

// 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.


Components

<Form>

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.

PropTypeDescription
formUseFormReturnForm instance from useFormCraft
onSubmit(data) => void | Promise<void>Called with validated data
classNamestringAdditional CSS classes

<Input>

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/>
PropTypeDefault
nameFieldPath<T>
controlControl<T>
labelstring
descriptionstring
leftIconReactNode
rightIconReactNode
loadingbooleanfalse
showPasswordTogglebooleanfalse

<Textarea>

<Textareaname="bio"control={form.control}label="Bio"showCountmaxLength={300}autoResize/>
PropTypeDefault
showCountbooleanfalse
maxLengthnumber
autoResizebooleanfalse

<Select>

<Selectname="country"control={form.control}label="Country"placeholder="Select a country"options={[{value: "us",label: "United States"},{value: "ph",label: "Philippines"},]}/>

<Checkbox>

<Checkboxname="acceptTerms"control={form.control}label="I agree to the Terms of Service"/>

<RadioGroup>

<RadioGroupname="plan"control={form.control}label="Plan"options={[{value: "free",label: "Free",description: "Up to 3 projects"},{value: "pro",label: "Pro",description: "Unlimited projects"},]}/>

<Switch>

<Switchname="notifications"control={form.control}label="Email notifications"description="Receive weekly digest emails"/>

<FileUpload>

Drag-and-drop with type/size validation.

<FileUploadname="avatar"control={form.control}label="Profile photo"accept="image/*"maxSize={5*1024*1024}// 5MB/>
PropTypeDefault
acceptstring
multiplebooleanfalse
maxSizenumber (bytes)
maxFilesnumber10

<FieldArray>

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>

<SubmitButton> & <FormActions>

<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>

Hooks

useFormCraft

Wraps react-hook-form with Zod resolver.

constform=useFormCraft({schema: myZodSchema,defaultValues: {email: ""},mode: "onBlur",// defaultreValidateMode: "onChange",});

useMultiStepForm

const{ step, totalSteps, next, back, goTo, isFirst, isLast, progress }=useMultiStepForm({steps: 3});

Per-step validation:

asyncfunctionhandleNext(){constvalid=awaitform.trigger(["firstName","lastName"]);if(valid)next();}

usePasswordStrength

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

useFormPersist(form,{key: "signup-form",storage: "sessionStorage",excludeFields: ["password"],});

useFormAutoSave

const{ isSaving, lastSaved }=useFormAutoSave(form,{onSave: async(data)=>awaitapi.saveDraft(data),debounce: 1500,});

Validation Schemas

Import pre-built Zod schemas or build your own with the helpers.

import{emailSchema,passwordSchema,phoneSchema,urlSchema,requiredString,mustBeChecked,loginSchema,registerSchema,contactSchema,}from"formcraft";

Build your own

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"),});

Accessibility

Every FormCraft component is built with accessibility first:

  • All inputs have associated <label> via htmlFor/id (auto-generated)
  • Error messages use role="alert" + aria-live="polite"
  • Inputs set aria-invalid when there's an error
  • aria-describedby links inputs to their descriptions and errors
  • RadioGroup uses role="radiogroup"
  • Switch uses role="switch" + aria-checked
  • FileUpload zone is keyboard-navigable (Enter / Space to open)
  • All interactive elements have :focus-visible styles
  • Shake animation is prefers-reduced-motion safe (CSS only)

TypeScript

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}/>

Dark Mode

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}"],};

Roadmap

  • DatePicker component
  • ComboBox / searchable select
  • PinInput (OTP)
  • FormSkeleton loading placeholder
  • React Native support

Contributing

PRs and issues are welcome! See CONTRIBUTING.md.

git clone https://github.com/formcraft/formcraft
pnpm install
pnpm dev # watch mode
pnpm test# run tests

License

MIT © FormCraft (JohnDev19)

About

Production-ready form components for React & Next.js. Schema validation, accessible UI, loading states — all wired up and ready to use. Stop rebuilding the same forms.

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages