@classytic/formkit
v1.0.3
Published
Headless, type-safe form generation engine for React. Schema-driven with full TypeScript support.
Maintainers
Readme
@classytic/formkit
Headless, type-safe form generation engine for React 18/19. Schema-driven with full TypeScript support.
Features
- 🎯 Headless - Bring your own UI components (Shadcn, MUI, Chakra, etc.)
- 📝 Schema-driven - Define forms with JSON/TypeScript schemas
- 🔒 Type-safe - Full TypeScript support with generics
- ⚡ React Hook Form - Built on top of the best form library
- 🎨 Variants - Support for multiple component variants
- 🔀 Conditional fields - Show/hide fields based on form values
- 📱 Responsive layouts - Multi-column grid layouts
- 🪶 Lightweight - ~6KB minified, tree-shakeable
Installation
npm install @classytic/formkit react-hook-form
# or
pnpm add @classytic/formkit react-hook-form
# or
yarn add @classytic/formkit react-hook-formQuick Start
1. Create Field Components
Each field component wraps your UI library with react-hook-form's Controller:
// components/form/form-input.tsx
"use client";
import { Controller } from "react-hook-form";
import type { FieldComponentProps } from "@classytic/formkit";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function FormInput({ control, name, label, placeholder, required }: FieldComponentProps) {
return (
<Controller
name={name}
control={control}
render={({ field, fieldState }) => (
<div className="space-y-2">
{label && (
<Label htmlFor={name}>
{label}
{required && <span className="text-red-500 ml-1">*</span>}
</Label>
)}
<Input {...field} id={name} placeholder={placeholder} />
{fieldState.error && (
<p className="text-sm text-red-500">{fieldState.error.message}</p>
)}
</div>
)}
/>
);
}2. Create Form Adapter
Register your components and layouts:
// lib/form-adapter.tsx
"use client";
import { FormSystemProvider, type ComponentRegistry, type LayoutRegistry } from "@classytic/formkit";
import { FormInput } from "@/components/form/form-input";
const components: ComponentRegistry = {
text: FormInput,
email: FormInput,
password: FormInput,
// Add more field types...
};
const layouts: LayoutRegistry = {
section: ({ title, description, children }) => (
<div className="space-y-4">
{title && <h3 className="text-lg font-semibold">{title}</h3>}
{description && <p className="text-muted-foreground">{description}</p>}
{children}
</div>
),
grid: ({ children, cols = 1 }) => (
<div className={`grid grid-cols-${cols} gap-4`}>{children}</div>
),
};
export function FormProvider({ children }: { children: React.ReactNode }) {
return (
<FormSystemProvider components={components} layouts={layouts}>
{children}
</FormSystemProvider>
);
}3. Use FormGenerator
// app/signup/page.tsx
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { FormGenerator, type FormSchema } from "@classytic/formkit";
import { FormProvider } from "@/lib/form-adapter";
// Validation schema
const signupSchema = z.object({
firstName: z.string().min(2),
lastName: z.string().min(2),
email: z.string().email(),
password: z.string().min(8),
});
type SignupData = z.infer<typeof signupSchema>;
// Form schema (type-safe!)
const formSchema: FormSchema<SignupData> = {
sections: [
{
title: "Personal Information",
cols: 2,
fields: [
{ name: "firstName", type: "text", label: "First Name", required: true },
{ name: "lastName", type: "text", label: "Last Name", required: true },
],
},
{
title: "Account",
fields: [
{ name: "email", type: "email", label: "Email", required: true },
{ name: "password", type: "password", label: "Password", required: true },
],
},
],
};
export default function SignupPage() {
const form = useForm<SignupData>({
resolver: zodResolver(signupSchema),
});
return (
<FormProvider>
<form onSubmit={form.handleSubmit(console.log)} className="space-y-8">
<FormGenerator schema={formSchema} control={form.control} />
<button type="submit">Sign Up</button>
</form>
</FormProvider>
);
}API Reference
FormGenerator
The main component that renders forms from a schema.
<FormGenerator
schema={formSchema} // Required: Form schema
control={form.control} // Required: React Hook Form control
disabled={false} // Optional: Disable all fields
variant="default" // Optional: Global variant
className="my-form" // Optional: Root element class
/>FormSchema
interface FormSchema<T extends FieldValues = FieldValues> {
sections: Section<T>[];
}
interface Section<T> {
id?: string; // Unique identifier
title?: string; // Section title
description?: string; // Section description
icon?: ReactNode; // Section icon
fields?: BaseField<T>[]; // Fields in this section
cols?: number; // Grid columns (1-6)
gap?: number; // Grid gap
variant?: string; // Section variant
className?: string; // Custom class
collapsible?: boolean; // Make section collapsible
defaultCollapsed?: boolean;
condition?: (control) => boolean; // Conditional rendering
render?: (props) => ReactNode; // Custom render function
}BaseField
interface BaseField<T> {
name: string; // Field name (required)
type: FieldType; // Field type (required)
label?: string; // Field label
placeholder?: string; // Placeholder text
helperText?: string; // Helper text below field
disabled?: boolean; // Disable field
required?: boolean; // Mark as required
readOnly?: boolean; // Read-only field
variant?: string; // Field variant
fullWidth?: boolean; // Span full grid width
className?: string; // Custom class
defaultValue?: unknown; // Default value
// Conditional rendering
condition?: (formValues: T) => boolean;
// For select/radio/checkbox
options?: FieldOption[];
// HTML input attributes
min?: number | string;
max?: number | string;
step?: number;
pattern?: string;
minLength?: number;
maxLength?: number;
rows?: number; // For textarea
multiple?: boolean; // For select/file
accept?: string; // For file input
autoComplete?: string;
autoFocus?: boolean;
// Custom props
[key: string]: unknown;
}FieldComponentProps
Props passed to your field components:
interface FieldComponentProps<T extends FieldValues = FieldValues> extends BaseField<T> {
field: BaseField<T>; // Full field config
control: Control<T>; // React Hook Form control
disabled?: boolean; // Merged disabled state
variant?: string; // Active variant
}ComponentRegistry
const components: ComponentRegistry = {
// Simple mapping
text: FormInput,
select: FormSelect,
// Variant-specific components
compact: {
text: CompactInput,
select: CompactSelect,
},
};LayoutRegistry
const layouts: LayoutRegistry = {
section: SectionLayout,
grid: GridLayout,
// Variant-specific layouts
compact: {
section: CompactSection,
},
};Advanced Features
Conditional Fields
{
name: "companyName",
type: "text",
label: "Company Name",
condition: (values) => values.accountType === "business",
}Variants
Apply different styles based on context:
// Register variant-specific components
const components = {
text: DefaultInput,
compact: {
text: CompactInput,
},
};
// Use variant in schema
const schema = {
sections: [{
variant: "compact", // All fields use compact variant
fields: [...]
}]
};
// Or per-field
{
name: "notes",
type: "text",
variant: "compact",
}Custom Section Render
{
title: "Payment",
render: ({ control, disabled }) => (
<StripeElements>
<CardElement />
<FormInput name="billingName" control={control} />
</StripeElements>
),
}Grouped Select Options
{
name: "country",
type: "select",
options: [
{
label: "North America",
options: [
{ value: "us", label: "United States" },
{ value: "ca", label: "Canada" },
],
},
{
label: "Europe",
options: [
{ value: "uk", label: "United Kingdom" },
{ value: "de", label: "Germany" },
],
},
],
}Type Exports
import type {
// Core
FormSchema,
FormGeneratorProps,
BaseField,
Section,
// Components
FieldComponentProps,
FieldComponent,
ComponentRegistry,
// Layouts
SectionLayoutProps,
GridLayoutProps,
LayoutComponent,
LayoutRegistry,
// Options
FieldOption,
FieldOptionGroup,
// Utility types
FieldType,
LayoutType,
Variant,
DefineField,
InferSchemaValues,
SchemaFieldNames,
} from "@classytic/formkit";Examples
See the example/shadcn directory for complete working examples with:
- Form components (Input, Select, Checkbox)
- Full adapter configuration
- Zod validation
- Conditional fields
- Multi-column layouts
- TypeScript integration
Browser Support
- React 18.0+
- React 19.0+
- All modern browsers
License
MIT © Classytic
