FormAdapter
UI adapters

shadcn/ui

Connect FormAdapter to the shadcn/ui components already owned by your app.

The shadcn adapter renders your generated components, preserving their source changes and styling. FormAdapter supplies form state, accessibility, and validation props through a small contract containing only the button variants and other props it renders.

Install the components

Initialize shadcn/ui with the primitive library you use, then add the components FormAdapter needs.

bunx shadcn@latest init -d --base base
# Or: bunx shadcn@latest init -d --base radix

bunx shadcn@latest add alert button checkbox field input native-select progress radio-group spinner textarea
bun add @formadapter/react @formadapter/shadcn zod

There is no FormAdapter CSS import. Keep the global styles and theme generated by shadcn/ui.

Connect your components

Use the entry point that matches components.json:

app/shadcn.tsx
"use client";

import { createShadcn } from "@formadapter/shadcn/baseui";
// Radix UI: import { createShadcn } from "@formadapter/shadcn/radix";

import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
import {
  Field, FieldContent, FieldDescription, FieldError, FieldGroup,
  FieldLabel, FieldLegend, FieldSet, FieldTitle,
} from "@/components/ui/field";
import { Input } from "@/components/ui/input";
import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select";
import { Progress } from "@/components/ui/progress";
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Spinner } from "@/components/ui/spinner";
import { Textarea } from "@/components/ui/textarea";

export const shadcn = createShadcn({
  Alert, AlertDescription, AlertTitle, Button, Checkbox,
  Field, FieldContent, FieldDescription, FieldError, FieldGroup,
  FieldLabel, FieldLegend, FieldSet, FieldTitle,
  Input, NativeSelect, NativeSelectOption, Progress,
  RadioGroup, RadioGroupItem, Spinner, Textarea,
});

export const ShadcnProvider = shadcn.Provider;

The component bundle is intentionally explicit. Missing or incompatible components fail type-checking where createShadcn is called instead of silently falling back to FormAdapter-owned lookalikes.

Provide it once

app/providers.tsx
"use client";

import { ShadcnProvider } from "./shadcn";

export function Providers({ children }: { children: React.ReactNode }) {
  return <ShadcnProvider>{children}</ShadcnProvider>;
}

Forms created with createForm from @formadapter/react now use the nearest provider. A child provider still replaces its parent for that subtree.

Adapter-bound forms and extensions

createShadcn returns one stable setup:

const { adapter, createForm, Provider } = shadcn;
  • Provider sets this component bundle for a subtree.
  • adapter can be extended or passed directly to one form.
  • createForm binds forms to this adapter without a provider.
import { z } from "zod";
import { shadcn } from "@/app/shadcn";

const Feedback = shadcn.createForm(z.object({
  email: z.email(),
  message: z.string().min(20),
})).configure({
  fields: { message: { control: "textarea" } },
});

If a customized component adds a required prop, wrap it locally and provide the wrapper. That keeps application-specific defaults in your design system rather than in FormAdapter.

On this page