Decision. The 2026 kits have all decoupled from the form engine — shadcn replaced the RHF-only
<Form>with a library-agnostic<Field>family and now documents RHF, TanStack Form and Formisch side by side [1][2]. So pick the engine on its merits, not the kit’s. Default: shadcn<Field>+ React Hook Form + Zod — the bridge is one<Controller>per controlled component and it’s boring. Choose Base UI or React Aria if you want a11y/native constraint validation to do most of the work; both work fine with an engine but neither needs one for simple forms [3][4]. Mantine is the only kit that still ships its own engine (@mantine/form), which is ergonomic inside Mantine and a dead end outside it [5].
The bridge is the cost
Every design-system Select / DatePicker / Combobox is a controlled component, so an “uncontrolled” engine has to adopt it. The shape is identical across engines — only the noun changes:
- RHF:
<Controller render={({field, fieldState}) => …}— spreadvalue/onChange/onBlur, and routefield.reftoinputRef(notref) or error-focus breaks; this is the classic MUI DatePicker trap [6] ⭐ 45k. - TanStack Form:
<form.Field>render prop →field.state.value+field.handleChange+field.handleBlur, and you hand-wirearia-invalid/data-invalidyourself [7].
Neither is a universal adapter: TanStack’s own UI-libraries guide concedes you adapt the event payload per component — MUI/Mantine want e.target.value, shadcn’s Checkbox wants onCheckedChange, Chakra’s needs !!details.checked coercion [8]. Budget one hand-written wrapper per control type, once, in a components/form/ folder. That is the whole story — anyone promising zero-glue is hiding a Controller.
Kits
| Kit | Built-in form story | Recommended pairing | Friction |
|---|---|---|---|
| shadcn/ui ⭐ 119k | <Field> = layout/a11y primitive only (labels, descriptions, errors, orientation); the old RHF-coupled <Form> still exists but is no longer the recommended path [2] |
RHF + Zod (or TanStack Form; Formisch documented, useActionState “coming soon”) [1] |
Low. Copy-paste source means you own the bridge; Select/Popover still need Controller |
| Base UI ⭐ 10k | Field.Root/Label/Control/Error/Validity + Form; native constraint validation (required, pattern), validationMode onSubmit/onBlur/onChange + debounce [9] |
Optional engine; docs ship first-party RHF <Controller> and TanStack form.Field recipes [3] |
Low-ish. Two validation systems can fight; remember inputRef for focus |
| React Aria Components ⭐ 16k | Strongest native story: validationBehavior="native"\|"aria", FieldError, server errors via Form validationErrors (pairs with Zod flatten().fieldErrors + useActionState) [4] |
Often no engine — docs say uncontrolled + built-in validation suffices for most forms; RHF via controlled bridge for complex ones [4] | Low if you go native; ⚠ verbose unstyled markup, and mixing RHF re-introduces double validation |
| MUI ⭐ 99k | None. FormControl supplies error/required context only; no state, no engine |
RHF + Controller (the de-facto community stack) |
Highest. MUI X pickers = ref/inputRef quirks, mask-vs-keyboard errors, per-picker adapters [6] |
| Mantine ⭐ 31k | Ships @mantine/form: form.getInputProps(name) spreads value/onChange/error/onBlur — every Mantine input, incl. dates, is compatible out of the box [5] |
@mantine/form if you stay in-kit; else react-hook-form-mantine ⭐ 98 (thin, low bus factor) |
Zero glue in-kit, but community complaints: whole-form re-render per keystroke and field names not type-enforced in setFieldValue/getInputProps [10] ⭐ 31k |
Native-validation angle. React Aria and Base UI both build on the HTML constraint API, so required/pattern/type=email produce real :invalid state and custom-rendered messages. That is the only path where the kit genuinely removes the engine [4][9]. The moment you add a schema engine on top, decide who owns validity — running both is the most common self-inflicted bug in this tier.
Custom-input contract (worth internalising, it’s the same everywhere): accept value, defaultValue, onChange, onBlur, error, and forward ref to something focusable. Mantine states it explicitly [11]; RHF, TanStack and Base UI all assume it.