npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@iamjohndev19/formcraft

v1.0.1

Published

A fully accessible, schema-validated, production-ready form component library for React & Next.js

Readme

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";
import type { LoginFormValues } from "formcraft";

export default function LoginPage() {
  const form = useFormCraft<LoginFormValues>({
    schema: loginSchema,
    defaultValues: { email: "", password: "" },
  });

  async function onSubmit(data: LoginFormValues) {
    await signIn(data); // your auth call
  }

  return (
    <Form form={form} onSubmit={onSubmit} className="fc-spaced">
      <Input name="email" control={form.control} label="Email" type="email" required />
      <Input name="password" control={form.control} label="Password" type="password" showPasswordToggle required />
      <SubmitButton loading={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.

<Form form={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 |


<Input>

Covers: text, email, password, number, search, tel, url.

<Input
  name="email"
  control={form.control}
  label="Email"
  type="email"
  placeholder="[email protected]"
  required
  leftIcon={<MailIcon />}
/>

<Input
  name="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 |


<Textarea>

<Textarea
  name="bio"
  control={form.control}
  label="Bio"
  showCount
  maxLength={300}
  autoResize
/>

| Prop | Type | Default | |------|------|---------| | showCount | boolean | false | | maxLength | number | — | | autoResize | boolean | false |


<Select>

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

<Checkbox>

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

<RadioGroup>

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

<Switch>

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

<FileUpload>

Drag-and-drop with type/size validation.

<FileUpload
  name="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 |


<FieldArray>

Dynamic repeating fields.

<FieldArray
  name="members"
  control={form.control}
  label="Team members"
  addLabel="Add member"
  minItems={1}
  maxItems={10}
  defaultItem={{ name: "", email: "" }}
>
  {(index) => (
    <>
      <Input name={`members.${index}.name`} control={form.control} label="Name" />
      <Input name={`members.${index}.email`} control={form.control} label="Email" />
    </>
  )}
</FieldArray>

<SubmitButton> & <FormActions>

<FormActions align="right">
  <button type="button" onClick={onCancel}>Cancel</button>
  <SubmitButton
    loading={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.

const form = useFormCraft({
  schema: myZodSchema,
  defaultValues: { email: "" },
  mode: "onBlur",          // default
  reValidateMode: "onChange",
});

useMultiStepForm

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

Per-step validation:

async function handleNext() {
  const valid = await form.trigger(["firstName", "lastName"]);
  if (valid) next();
}

usePasswordStrength

const strength = usePasswordStrength(form.watch("password"));
// { score: 3, label: "Good", color: "text-blue-500", percentage: 60 }

<div style={{ width: `${strength.percentage}%` }} className={strengthBarColor} />
<span className={strength.color}>{strength.label}</span>

useFormPersist

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

useFormAutoSave

const { isSaving, lastSaved } = useFormAutoSave(form, {
  onSave: async (data) => await api.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";

const checkoutSchema = 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
<Input name="emaill" control={form.control} />

Dark Mode

FormCraft supports Tailwind's dark: class strategy. Ensure your Tailwind config has:

// tailwind.config.js
module.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)