JSPM

  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 200
  • Score
    100M100P100Q73606F
  • License MIT

Headless, type-safe form generation engine for React. Schema-driven with full TypeScript support.

Package Exports

  • @classytic/formkit
  • @classytic/formkit/package.json

Readme

@classytic/formkit

Headless, type-safe form generation engine for React 18/19. Schema-driven with full TypeScript support.

npm License: MIT TypeScript

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-form

Quick 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