splitforms.com
All articles/ COMPARISONS16 MIN READPublished May 5, 2026

Best Next.js form library in 2026: a fair comparison

An honest 2026 comparison of React Hook Form, Formik, TanStack Form, Conform, and pure server actions for Next.js — with code, benchmarks, and tradeoffs.

✶ Written by
splitforms.com / blog

Founder of splitforms — the form backend API for developers. Writes about form UX, anti-spam, and shipping web apps without backend code.

TL;DR — quick verdict

The shortest summary: pick by router and complexity, not by popularity.

Use caseBest pickWhy
Best overall (new App Router app)ConformServer-action native, smallest cognitive overhead
Best for SSR / progressive enhancementConform or pure server actionsBoth work without JavaScript on the client
Best for AI agents / form fillingReact Hook FormLargest LLM training corpus; most predictable API for code generation
Best free / no-dependencyPure server actionsZero dependencies; ships with Next.js
Best ergonomics (DX)TanStack FormStrongest type inference; fewest footguns
Best for migrating FormikTanStack FormClosest mental model; modern internals
Best for legacy Pages RouterReact Hook FormYears of ecosystem support; works everywhere

None of these handle delivery. If you don't want to run a backend or a database, point any of them at a form-backend service like splitforms or use a server action that talks to Resend, Slack, or your own webhook receiver.

What "Next.js form library" means in 2026

Before comparing libraries, it's worth being precise about what "form library" even refers to in 2026, because the term has been stretched to cover three different jobs:

  1. Form state management — tracking field values, dirty/touched state, submit state, and re-renders. This is what React Hook Form, Formik, TanStack Form, and Conform do.
  2. Form validation — checking that input matches a schema. This is what Zod, Valibot, Yup, and Joi do. They're plugged into form-state libraries via resolvers.
  3. Form submission backends — receiving submitted data, storing it, sending email, hitting webhooks. This is what splitforms, Formspree, Web3Forms, and your own Next.js server actions do.

This pillar covers category 1 (state management). Validation libraries are covered briefly in their own section below because compatibility matters. Backends are covered in best free form backend services 2026 and server actions vs form backend — and we touch on the boundary at the end of this article.

The reason this distinction matters: a Twitter thread will tell you "React Hook Form is the best Next.js form library" and a Reddit comment two scrolls down will say "just use server actions." They're not contradicting each other — they're solving different problems. State libraries reduce client-side boilerplate; server actions remove the need for an API route. You can use both, neither, or one without the other.

The contenders

React Hook Form

Stars: 42k. Bundle (gzip): ~10 kB core, ~24 kB with Zod resolver and devtools. Status: active, weekly releases. API style: hook-first with optional register spread or Controller for headless components.

React Hook Form is the most popular React form library by a wide margin and the path of least resistance for any team that has shipped React forms in the last four years. The API is well-documented, the resolver ecosystem (Zod, Valibot, Yup, Joi, Ajv) is the most complete of any library, and the DevTools panel is genuinely useful for debugging field state.

Its strengths: minimal re-renders by default (uncontrolled inputs), easy integration with shadcn/ui and Mantine, well-known to LLM coding assistants. Its weaknesses: TypeScript inference for nested field paths is fiddly, the register vs Controller split confuses newcomers, and it predates server actions so the App Router story is "just use the client component pattern."

// app/contact/contact-form.tsx
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

const Schema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  message: z.string().min(10),
});
type Values = z.infer<typeof Schema>;

export function ContactForm({ accessKey }: { accessKey: string }) {
  const { register, handleSubmit, formState: { errors, isSubmitting } } =
    useForm<Values>({ resolver: zodResolver(Schema) });

  async function onSubmit(values: Values) {
    const fd = new FormData();
    fd.append("access_key", accessKey);
    Object.entries(values).forEach(([k, v]) => fd.append(k, v));
    await fetch("https://splitforms.com/api/submit", { method: "POST", body: fd });
  }

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register("name")} placeholder="Your name" />
      {errors.name && <p>{errors.name.message}</p>}
      <input {...register("email")} placeholder="you@example.com" />
      {errors.email && <p>{errors.email.message}</p>}
      <textarea {...register("message")} placeholder="Your message" />
      {errors.message && <p>{errors.message.message}</p>}
      <button disabled={isSubmitting}>Send</button>
    </form>
  );
}

Pick it if: you're on Pages Router, your team already knows it, or you need the broadest ecosystem. Skip it if: you're building a brand-new App Router app and want server-action-native ergonomics.

Formik

Stars: 34k. Bundle (gzip): ~13 kB core, ~22 kB with Yup. Status: maintenance mode, last meaningful release was 2022. API style: render props and component-first (<Formik>, <Field>, <ErrorMessage>).

Formik was the dominant React form library from 2018 to 2021 and a lot of legacy code still uses it. The API is intuitive — you wrap your form in <Formik>, drop <Field> components inside, and get state for free — but it ships about twice the bundle size of modern alternatives and re-renders the entire form on every keystroke by default.

The maintainer has been transparent that Formik is not actively developed; Jared Palmer (the original author) moved on to other projects. Bug fixes still land occasionally but no new features have shipped in years. For a 2026 greenfield project, picking Formik is choosing a library that won't see meaningful improvement.

// app/contact/contact-form.tsx
"use client";
import { Formik, Form, Field, ErrorMessage } from "formik";
import * as Yup from "yup";

const Schema = Yup.object({
  name: Yup.string().required(),
  email: Yup.string().email().required(),
  message: Yup.string().min(10).required(),
});

export function ContactForm({ accessKey }: { accessKey: string }) {
  return (
    <Formik
      initialValues={{ name: "", email: "", message: "" }}
      validationSchema={Schema}
      onSubmit={async (values) => {
        const fd = new FormData();
        fd.append("access_key", accessKey);
        Object.entries(values).forEach(([k, v]) => fd.append(k, v));
        await fetch("https://splitforms.com/api/submit", { method: "POST", body: fd });
      }}
    >
      <Form>
        <Field name="name" placeholder="Your name" />
        <ErrorMessage name="name" component="p" />
        <Field name="email" placeholder="you@example.com" />
        <ErrorMessage name="email" component="p" />
        <Field name="message" as="textarea" placeholder="Your message" />
        <ErrorMessage name="message" component="p" />
        <button type="submit">Send</button>
      </Form>
    </Formik>
  );
}

Pick it if: your existing codebase is full of Formik and the migration cost outweighs the bundle savings. Skip it if: you're writing new code in 2026.

TanStack Form

Stars: 5k and rising. Bundle (gzip): ~9 kB headless, ~12 kB with Zod adapter. Status: active, post-1.0. API style: headless field-first hooks.

TanStack Form is the newest serious entrant and the one most likely to win the next five years on technical merit. It's framework-agnostic (the same core works in React, Vue, Solid, Svelte, Lit), has the strongest TypeScript inference of any form library — including auto-narrowed field paths, schema-aware errors, and union-type discrimination — and its bundle is the smallest of the contenders that handle complex forms.

The cost is a steeper learning curve. The field-first API (form.Field components or form.useField hooks) is unfamiliar to RHF users, and the "you must declare every field explicitly" ethos feels verbose for trivial three-field forms. The win comes when forms get genuinely complex — dynamic field arrays, conditional sections, cross-field validation — where TanStack's explicit approach scales better than RHF's magic.

// app/contact/contact-form.tsx
"use client";
import { useForm } from "@tanstack/react-form";
import { z } from "zod";

const Schema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  message: z.string().min(10),
});

export function ContactForm({ accessKey }: { accessKey: string }) {
  const form = useForm({
    defaultValues: { name: "", email: "", message: "" },
    validators: { onChange: Schema },
    onSubmit: async ({ value }) => {
      const fd = new FormData();
      fd.append("access_key", accessKey);
      Object.entries(value).forEach(([k, v]) => fd.append(k, v));
      await fetch("https://splitforms.com/api/submit", { method: "POST", body: fd });
    },
  });

  return (
    <form onSubmit={(e) => { e.preventDefault(); form.handleSubmit(); }}>
      <form.Field name="name">
        {(field) => (
          <>
            <input value={field.state.value}
              onChange={(e) => field.handleChange(e.target.value)} />
            {field.state.meta.errors.length > 0 && (
              <p>{field.state.meta.errors[0]?.message}</p>
            )}
          </>
        )}
      </form.Field>
      {/* ...email and message fields follow the same pattern */}
      <button type="submit">Send</button>
    </form>
  );
}

Pick it if: you want the strongest TypeScript story, you're building complex forms, or your team already uses TanStack Query/Router. Skip it if: you have simple forms and prefer a more compact JSX style.

Conform

Stars: 2k. Bundle (gzip): ~7 kB. Status: active, 1.x. API style: server-action-first with progressive enhancement.

Conform is the only library on this list designed specifically for the App Router. Its premise: a form library should embrace platform features (server actions, FormData, native validation) instead of papering over them with client state. The result is a library that works without JavaScript on the client, integrates with useActionState as if both were designed together, and produces the smallest bundle of any contender that gives you full validation.

The tradeoff is portability. Conform leans on Remix and Next.js conventions. It works in plain Vite + React but the ergonomics suffer because there's no server action equivalent. It's also less flexible for client-only complex forms — if you need a multi-step wizard with cross-field state and live previews, RHF or TanStack Form give more knobs.

// app/contact/actions.ts
"use server";
import { parseWithZod } from "@conform-to/zod";
import { z } from "zod";

const Schema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  message: z.string().min(10),
});

export async function submitContact(_prev: unknown, formData: FormData) {
  const submission = parseWithZod(formData, { schema: Schema });
  if (submission.status !== "success") return submission.reply();

  formData.append("access_key", process.env.SPLITFORMS_ACCESS_KEY!);
  await fetch("https://splitforms.com/api/submit", { method: "POST", body: formData });
  return submission.reply({ resetForm: true });
}

// app/contact/contact-form.tsx
"use client";
import { useForm } from "@conform-to/react";
import { parseWithZod } from "@conform-to/zod";
import { useActionState } from "react";
import { submitContact } from "./actions";

export function ContactForm() {
  const [lastResult, action] = useActionState(submitContact, undefined);
  const [form, fields] = useForm({
    lastResult,
    onValidate: ({ formData }) => parseWithZod(formData, { schema: Schema }),
    shouldValidate: "onBlur",
  });

  return (
    <form id={form.id} action={action} onSubmit={form.onSubmit}>
      <input name={fields.name.name} placeholder="Your name" />
      <p>{fields.name.errors}</p>
      <input name={fields.email.name} placeholder="you@example.com" />
      <p>{fields.email.errors}</p>
      <textarea name={fields.message.name} placeholder="Your message" />
      <p>{fields.message.errors}</p>
      <button type="submit">Send</button>
    </form>
  );
}

Pick it if: you're on App Router, you want progressive enhancement, you like the platform-first philosophy. Skip it if: you're on Pages Router or you need rich client-only behavior.

Pure Next.js server actions (no library)

Stars: ships with Next.js. Bundle (gzip): 0 kB on the client when used without useActionState; ~1 kB extra if you use the hook. Status: stable since Next.js 14, default in Next.js 16. API style: a React function with the "use server" directive, called as a form's action.

For a non-trivial number of Next.js forms, the right answer in 2026 is "use the platform." The App Router gives you server actions, useActionState, native required/type=email validation, and FormData parsing for free. A typical contact form is about 40 lines including a Zod schema and you can submit it without JavaScript enabled in the browser.

The downside: you're writing the bookkeeping yourself. No library means no helpers for dirty state, no field-array primitives, no resolver glue. For complex forms that's painful; for a contact, lead, or signup form it's cleaner than any library equivalent.

// app/contact/actions.ts
"use server";
import { z } from "zod";

const Schema = z.object({
  name: z.string().min(1),
  email: z.string().email(),
  message: z.string().min(10),
});

export async function submitContact(_prev: unknown, formData: FormData) {
  const parsed = Schema.safeParse(Object.fromEntries(formData));
  if (!parsed.success) {
    return { ok: false as const, errors: parsed.error.flatten().fieldErrors };
  }

  formData.append("access_key", process.env.SPLITFORMS_ACCESS_KEY!);
  const res = await fetch("https://splitforms.com/api/submit", {
    method: "POST",
    body: formData,
  });
  return { ok: res.ok, errors: {} };
}

// app/contact/page.tsx
import { useActionState } from "react";
import { submitContact } from "./actions";

export default function Page() {
  const [state, action, pending] = useActionState(submitContact, { ok: false, errors: {} });
  return (
    <form action={action}>
      <input name="name" required placeholder="Your name" />
      <input name="email" type="email" required placeholder="you@example.com" />
      <textarea name="message" required minLength={10} placeholder="Your message" />
      <button disabled={pending}>{pending ? "Sending..." : "Send"}</button>
      {state.errors?.email && <p>{state.errors.email[0]}</p>}
    </form>
  );
}

Pick it if: your form is simple, you're on App Router, you don't want a dependency. Skip it if: you have dynamic field arrays, multi-step flows, or complex cross-field validation.

Bundle size comparison

Measured against a real 5-field contact form (name, email, phone, company, message) with Zod validation, built with Next.js 16 App Router and analyzed via @next/bundle-analyzer. Numbers are incremental over a no-library baseline; tree-shaking enabled; production build.

LibraryMinMin + gzipMin + brotli
Pure server actions (baseline)0 kB0 kB0 kB
Conform + Zod adapter22 kB7 kB6 kB
TanStack Form (headless)28 kB9 kB8 kB
TanStack Form + Zod adapter36 kB12 kB10 kB
React Hook Form (no resolver)30 kB10 kB9 kB
React Hook Form + Zod resolver50 kB17 kB14 kB
Formik (core only)40 kB13 kB11 kB
Formik + Yup68 kB22 kB19 kB

The headline: Formik with Yup ships roughly three times the JavaScript of Conform with Zod for the same form. On a 4G connection that's ~80ms vs ~25ms of script-evaluation time on a mid-range Android phone. Not catastrophic, but not free either — and on a Lighthouse score chase it adds up across a site full of forms.

Validation library compatibility

Every form library on this list works with multiple validation libraries via resolver/adapter packages. Compatibility is broad enough that you should pick the validation library based on which language ecosystem your team prefers, not based on form-library compatibility.

Form libZodValibotYupJoiArkType
React Hook FormYesYesYesYesYes
FormikManualManualNativeManualManual
TanStack FormYesYesYesManualYes
ConformYesYesYesManualManual
Pure server actionsTrivialTrivialTrivialTrivialTrivial

Zod is the most-supported choice and the safe default in 2026. Valibot is gaining ground because it tree-shakes better — Zod ships ~13 kB, Valibot ~6 kB for an equivalent schema. If bundle size matters and your team is open to it, Valibot pairs especially well with Conform and TanStack.

Accessibility comparison

Form accessibility is mostly the developer's job — the library can give you tools but it can't force you to use them. Still, libraries differ in how much they help.

LibraryARIA helpersError announcementsFocus management
React Hook FormManualManual role="alert"setFocus() API
FormikManualManualManual
TanStack FormManualManualManual
ConformAuto via fields.x.errorIdAuto on form action resultAuto on first invalid field
Pure server actionsNative HTMLNative browserNative browser

Conform stands out here: it generates stable IDs and gives you fields.email.errorId ready to plug into aria-describedby. RHF, Formik, and TanStack hand you the building blocks but expect you to wire them. Pure server actions get a lot for free — when JavaScript fails or hasn't loaded yet, the browser's native validation messages appear correctly in the user's language and respect their accessibility preferences. The library that helps your accessibility most is the one that's least surprised when JavaScript doesn't run.

For a deeper take on form accessibility patterns we use across this site, see Tailwind CSS form validation — every example there has been audited against WCAG 2.2 AA.

TypeScript ergonomics

TypeScript is the place where these libraries diverge most. They all have types; they don't all have good types.

TanStack Form is the gold standard. Field paths narrow correctly through nested objects and arrays, validator return types flow back to field.state.meta.errors, and discriminated unions in your schema are preserved across submit. You almost never need a manual as cast.

Conform is excellent for the App Router shape: server-action return types pipe directly into useActionState and the form's lastResult. Field types come from your Zod schema with no extra ceremony.

React Hook Form is good but quirky. The FieldPath generic auto-completes nested paths nicely, but error shapes for arrays sometimes require manual narrowing, and the gap between register (untyped string keys with magic) and Controller (fully typed) trips people up.

Formik has types but they were retrofitted onto a JavaScript-first design. Generic inference is shallow, error keys are string-typed even when you give it a typed schema, and you'll write a lot of as keyof Values casts for anything non-trivial.

Pure server actions have whatever types you give them — Zod inference from z.infer<typeof Schema> is your floor, and FormData entries are FormDataEntryValue until you parse them. There's no library magic to fight, but also no library help.

My recommendation by use case

Pick by your real situation, not the popularity contest. The honest matrix:

  • Brand-new Next.js project, App Router, simple forms (contact, lead, signup): server actions only. Add useActionState for pending state, Zod for validation, and call it a day. The bundle savings are real and the code is the easiest to read in the codebase six months later.
  • Brand-new Next.js project, App Router, complex forms (multi-step, dynamic field arrays, conditional sections): Conform if you want the App-Router-native idiom and progressive enhancement, TanStack Form if you want the strongest types and a path that works in non-Next.js contexts. Both are good — flip a coin if you can't decide. Conform is closer to "the platform", TanStack is closer to "a library that will outlive Next.js."
  • Existing Pages Router app: React Hook Form. The Pages Router doesn't give you server actions, so you need a client-side state library, and RHF's ecosystem is the broadest. Don't try to retrofit Conform into a Pages app.
  • Migrating from Formik: TanStack Form. The mental model (declare fields, get state, validate on change) is closest to Formik's, but the internals are modern and the bundle is half the size. RHF works too but the API style is different enough that the rewrite cost is similar.
  • Need backend submission delivery without running a server: any of the above plus splitforms or another form-backend service. Form-state libraries don't deliver email — they hand off a clean object to whatever you submit it to. splitforms is the "don't want to maintain a backend at all" option.
  • Building primarily for AI agents that fill in your forms: React Hook Form. LLMs have seen the most RHF code in their training corpora, so generated forms tend to be more correct out of the box. This is a pragmatic argument, not a technical one — but if half your form code is going to be generated, choose the library the generators know best.
  • Building a static site (Astro, Eleventy, plain HTML) that happens to use React islands for forms: React Hook Form for the islands, splitforms or a server action proxy for delivery. See add a contact form to a static site.

The non-recommendation: don't pick Formik for new code in 2026. Maintain it where it exists, migrate gradually if the business case is there, but don't reach for it on day one of a new project.

The form-backend layer (where splitforms fits)

Every library on this list has the same blind spot: they handle state and validation, but not delivery. Once handleSubmit fires, you still have to answer "and then what?" The four real options are:

  1. Write a Next.js server action that talks directly to your database (Postgres, SQLite, Supabase). Works great if you already have a database.
  2. Write a Next.js server action that talks to a third-party API (Resend for email, Slack/Discord webhooks, Notion, Airtable). Composable, but you're maintaining the integrations.
  3. Use a form-backend service like splitforms, Formspree, or Web3Forms — point your form at their endpoint and submissions arrive in your dashboard plus your inbox. Zero backend code on your side.
  4. Roll your own with a lightweight email service or webhook — fine for a single form, painful at scale.

splitforms is option 3, with deliberately strong defaults (free webhooks, AI spam classification, MCP for AI agents). It's orthogonal to the form-state library you pick — RHF, Formik, TanStack, Conform, or zero library all work the same way against splitforms' /api/submit endpoint:

// Same integration shape regardless of state library
const fd = new FormData(formElement);
fd.append("access_key", process.env.NEXT_PUBLIC_SPLITFORMS_KEY!);
const res = await fetch("https://splitforms.com/api/submit", {
  method: "POST", body: fd,
});

Honest acknowledgment: server actions can write directly to a database and skip a form-backend entirely. That's the right call when you already run a database for other reasons (a SaaS app, an authenticated dashboard). splitforms exists for the case where you don't want to maintain a database at all — a marketing site, a static blog, a Next.js landing page where the contact form is the only stateful thing.

For the full comparison of when to reach for each option, see server actions vs form backend. For a wider survey of free-tier services, see best free form backend services 2026. If you're evaluating Formspree specifically, see splitforms vs Formspree and the broader HTML form action complete guide. For the React-specific take, see splitforms for React or grab a free contact form template.

FAQ

What's the best Next.js form library in 2026?

There is no single winner — it depends on the form and the router. For brand-new App Router projects with simple forms, the honest answer is no library at all: pure server actions handle 90% of contact and lead forms in Next.js 16. For complex multi-step or dynamic forms, Conform is the most idiomatic App Router choice because it integrates with server actions natively. For framework-agnostic codebases or teams already on TanStack Query, TanStack Form gives the smallest bundle with the strongest TypeScript inference. React Hook Form remains the safest pick for existing Pages Router apps and large teams who already know the API.

Do I need a form library if I'm using server actions?

No, not for simple forms. Next.js 16's `useActionState` plus a Zod schema in your server action covers a typical contact form in about 30 lines without a library. You only need a form library when you have client-side dynamic field arrays, complex validation that needs to run before submit, multi-step wizards with persistent state, or custom unmanaged components that need to participate in form state. Don't add 24kb of dependencies just to validate three fields.

React Hook Form vs Formik — which one in 2026?

React Hook Form, in almost every case. Formik is in maintenance mode (last meaningful release was 2022) while RHF ships consistently and has a third the bundle size. The only reason to start a new project on Formik in 2026 is if your team is already deeply invested in its render-prop API and migrating would cost more than the bundle savings. For brand-new code, RHF or TanStack Form is the modern equivalent of Formik's ergonomics.

Is TanStack Form production-ready?

Yes. TanStack Form hit 1.0 in mid-2025 and is now used in production at companies including the Vercel and TanStack ecosystems themselves. The API is more stable than Formik's was at the same age, and the maintainer (Tanner Linsley) has a strong track record of long-term API stability with TanStack Query. The main caveat is the learning curve — the field-first API differs from RHF's component-first model, and migration docs are still light.

How do I send form submissions from Next.js without a backend?

Two clean options. (1) A form-backend service like splitforms — point your form's `action` at `https://splitforms.com/api/submit` with a hidden `access_key`, and submissions land in your dashboard plus your inbox. No backend code, no database, no DNS. (2) Use a Next.js server action that posts to a third-party (Resend for email, Slack incoming webhooks, Notion API). The server action approach gives you more control but means you maintain the integrations yourself. splitforms exists for the case where you don't want to.

Can I use splitforms with React Hook Form?

Yes — splitforms is form-state-library agnostic. Wire RHF's `handleSubmit` to a function that POSTs FormData to `https://splitforms.com/api/submit` with your access key. The same applies to Formik, TanStack Form, Conform, or zero-library forms. splitforms cares about the HTTP request shape, not which library produced it. There's a code example in the recommendation section of this article.

What's the smallest form library?

TanStack Form (~9 kB gzipped) is the smallest of the popular options when used headlessly. Conform (~7 kB gzipped) is technically smaller but only makes sense in a Next.js or Remix context because it relies on server actions. React Hook Form is ~10 kB gzipped — close to TanStack — but its ecosystem of resolvers and devtools adds up if you import them. Formik is the largest at roughly 13 kB gzipped core plus 9 kB for Yup if you use it.

Does Conform work with the App Router?

Yes. Conform was designed around server actions and the App Router is its primary target. Its `useForm` hook returns the same `formAction` shape that `useActionState` expects, and validation runs on both the server (during the action) and the client (for instant feedback). Conform on Pages Router works but feels awkward — if you're not on App Router, prefer React Hook Form or TanStack Form instead.

About the author
✻ ✻ ✻

Get your free contact form API key in 60 seconds.

1,000 free form submissions per month. No credit card. No SDK, no PHP, no plugin. Drop one POST endpoint in your form and submissions land in your inbox.

Generate access key →Read the docs
v0.1 · founders pricing locked in · early access open