New Research: Supply Chain Attack on Axios Pulls Malicious Dependency from npm.Details →
Socket
Book a DemoSign in
Socket

@classytic/formkit

Package Overview
Dependencies
Maintainers
1
Versions
9
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@classytic/formkit

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

latest
Source
npmnpm
Version
1.3.1
Version published
Maintainers
1
Created
Source

@classytic/formkit

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

npm License: MIT TypeScript

Features

  • Minimal boilerplate - useFormKit hook: 5 lines to set up a complete form
  • Headless - Bring your own UI components (Shadcn, MUI, Chakra, etc.)
  • Schema-driven - Define forms with JSON/TypeScript schemas, defaults extracted automatically
  • Type-safe - Full TypeScript support with generics
  • React Hook Form - Built on top of the best form library, referentially stable return values
  • React 19 - Uses modern React 19 patterns (Context as provider, ref as prop)
  • Server Components - Dedicated @classytic/formkit/server entry point for RSC
  • Variants - Support for multiple component variants
  • Conditional fields - Show/hide fields based on form values (function, DSL rules, AND/OR logic)
  • Responsive layouts - Multi-column grid layouts
  • Accessibility - Auto-generated fieldId, error, and fieldState props
  • Validation helpers - buildValidationRules generates RHF rules from schema props
  • Lightweight - ~7KB gzipped, tree-shakeable

Requirements

  • React 19.0+ (React 18 is not supported)
  • React Hook Form 7.55.0+

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 receives FieldComponentProps including error, fieldId, and the full field config:

// 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,
  field,
  label,
  placeholder,
  required,
  error,
  fieldId,
}: FieldComponentProps) {
  return (
    <Controller
      name={field.name}
      control={control}
      render={({ field: rhfField }) => (
        <div className="space-y-2">
          {label && (
            <Label htmlFor={fieldId}>
              {label}
              {required && <span className="text-red-500 ml-1">*</span>}
            </Label>
          )}
          <Input {...rhfField} id={fieldId} placeholder={placeholder} />
          {error && (
            <p className="text-sm text-red-500">{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 { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { FormGenerator, useFormKit, type FormSchema } from "@classytic/formkit";
import { FormProvider } from "@/lib/form-adapter";

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

const formSchema: FormSchema<SignupData> = {
  sections: [
    {
      title: "Personal Information",
      cols: 2,
      fields: [
        { name: "firstName", type: "text", label: "First Name", required: true, defaultValue: "" },
        { name: "lastName", type: "text", label: "Last Name", required: true, defaultValue: "" },
      ],
    },
    {
      title: "Account",
      fields: [
        { name: "email", type: "email", label: "Email", required: true, defaultValue: "" },
        { name: "password", type: "password", label: "Password", required: true, defaultValue: "" },
      ],
    },
  ],
};

export default function SignupPage() {
  const { handleSubmit, generatorProps } = useFormKit({
    schema: formSchema,
    resolver: zodResolver(signupSchema),
  });

  return (
    <FormProvider>
      <form onSubmit={handleSubmit(console.log)} className="space-y-8">
        <FormGenerator {...generatorProps} />
        <button type="submit">Sign Up</button>
      </form>
    </FormProvider>
  );
}

API Reference

useFormKit

Convenience hook that combines schema default extraction with react-hook-form setup. Returns all useForm methods plus ready-to-spread generatorProps.

Referentially stable — the return value preserves the original useForm object identity across re-renders, so it's safe to use in useEffect dependency arrays.

import { useFormKit, FormGenerator } from "@classytic/formkit";

const form = useFormKit({
  schema: formSchema,
  resolver: zodResolver(validationSchema), // optional
  defaultValues: { email: "pre@fill.com" }, // optional overrides
  disabled: false,                          // optional
  variant: "compact",                       // optional
  className: "my-form",                     // optional
  mode: "onBlur",                           // any useForm option
});

const { handleSubmit, generatorProps } = form;

// Safe to use in useEffect deps — form is referentially stable
useEffect(() => {
  if (open) form.reset(defaults);
}, [open, form]);

return (
  <form onSubmit={handleSubmit(onSubmit)}>
    <FormGenerator {...generatorProps} />
    <button type="submit">Submit</button>
  </form>
);

Schema defaultValue fields are automatically extracted and merged with any explicit defaultValues you provide (explicit values take priority).

generatorProps is memoized — it only recomputes when schema, control, disabled, variant, or className change.

FormGenerator

The main component that renders forms from a schema. Supports React 19 ref as a regular prop.

<FormGenerator
  schema={formSchema}       // Required: Form schema
  control={form.control}    // Optional: React Hook Form control (or wrap in <FormProvider>)
  disabled={false}           // Optional: Disable all fields
  variant="default"          // Optional: Global variant
  className="my-form"        // Optional: Root element class
  ref={formRef}              // Optional: Ref to the root <div> (React 19 ref-as-prop)
/>

FormSchema

interface FormSchema<T extends FieldValues = FieldValues> {
  sections: Section<T>[];
}

Section

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;
  nameSpace?: string;           // Prefix for nested object fields (e.g. "address")

  // Conditional rendering (function, DSL rule, or ConditionConfig)
  condition?: Condition<T>;

  // Custom render function (bypasses grid layout)
  render?: (props: SectionRenderProps<T>) => ReactNode;
}

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?: Condition<T>;
  watchNames?: string | string[];  // Optimize useWatch performance

  // Dynamic options loading
  loadOptions?: (formValues: Partial<T>) => Promise<FieldOption[]> | FieldOption[];
  debounceMs?: number;

  // For array/grouped types
  itemFields?: BaseField<T>[];

  // 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;
  multiple?: boolean;
  accept?: string;
  autoComplete?: string;
  autoFocus?: boolean;

  // Custom render override
  render?: (props: FieldComponentProps<T>) => ReactNode;

  // Arbitrary extra props for custom components
  customProps?: Record<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
  error?: FieldError;           // Field error from react-hook-form
  fieldState?: {                // Field state metadata
    invalid: boolean;
    isDirty: boolean;
    isTouched: boolean;
    isValidating: boolean;
    error?: FieldError;
  };
  fieldId: string;              // Generated ID for label-input association (e.g. "formkit-field-email")
}

Condition Types

Conditions can be a function, a DSL rule, an array of rules (AND), or a ConditionConfig (AND/OR):

// Function condition
condition: (values) => values.accountType === "business"

// Single DSL rule
condition: { watch: "country", operator: "===", value: "US" }

// Array of rules (AND - all must match)
condition: [
  { watch: "country", operator: "===", value: "US" },
  { watch: "age", operator: "truthy" },
]

// ConditionConfig with OR logic
condition: {
  rules: [
    { watch: "country", operator: "===", value: "US" },
    { watch: "country", operator: "===", value: "CA" },
  ],
  logic: "or",
}

Supported operators: ===, !==, in, not-in, truthy, falsy

Nested paths: DSL rules support dot-notation paths like "address.city" for nested form values.

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,
  },
};

extractDefaultValues

Extracts default values from a schema. Server-safe (no hooks).

import { extractDefaultValues } from "@classytic/formkit"; // or /server

const defaults = extractDefaultValues(formSchema);
// { firstName: "", lastName: "", email: "", password: "" }

// Use with react-hook-form
const form = useForm({ defaultValues: defaults });

Respects nameSpace prefixes and group itemFields defaults.

buildValidationRules

Generates react-hook-form validation rules from a field's schema props. Server-safe (no hooks).

import { buildValidationRules } from "@classytic/formkit"; // or /server

function FormInput({ field, control, error, fieldId }: FieldComponentProps) {
  const rules = buildValidationRules(field);
  return (
    <Controller
      name={field.name}
      control={control}
      rules={rules}
      render={({ field: rhf }) => <input {...rhf} id={fieldId} />}
    />
  );
}

Maps required, min, max, minLength, maxLength, and pattern from the field schema to RHF-compatible rules with auto-generated error messages.

Server Components

The @classytic/formkit/server entry point exports server-safe utilities with no React hooks or client-side code:

import {
  cn,
  defineSchema,
  defineField,
  defineSection,
  evaluateCondition,
  extractWatchNames,
  extractDefaultValues,
  buildValidationRules,
} from "@classytic/formkit/server";

// Type-only imports also available
import type {
  FormSchema,
  BaseField,
  Section,
  ConditionRule,
  ConditionConfig,
} from "@classytic/formkit/server";

Use this entry point in React Server Components to define schemas, evaluate conditions, or use cn without pulling in client-side code.

Advanced Features

Conditional Fields (Function)

{
  name: "companyName",
  type: "text",
  label: "Company Name",
  condition: (values) => values.accountType === "business",
}

Conditional Fields (DSL Rules)

{
  name: "stateField",
  type: "select",
  label: "State",
  condition: { watch: "country", operator: "===", value: "US" },
  watchNames: ["country"], // Optimizes re-renders
}

Conditional Sections

{
  title: "Business Details",
  condition: (values) => values.accountType === "business",
  fields: [
    { name: "companyName", type: "text", label: "Company" },
    { name: "taxId", type: "text", label: "Tax ID" },
  ],
}

OR Conditions

{
  name: "taxField",
  type: "text",
  condition: {
    rules: [
      { watch: "country", operator: "===", value: "US" },
      { watch: "country", operator: "===", value: "CA" },
    ],
    logic: "or",
  },
}

Nested Path Conditions

DSL rules resolve dot-notation paths for nested form values:

{
  name: "zipCode",
  type: "text",
  condition: { watch: "address.country", operator: "===", value: "US" },
}

Namespace Support

Prefix all field names in a section with a namespace for nested objects:

{
  nameSpace: "address",
  fields: [
    { name: "street", type: "text" },  // Becomes "address.street"
    { name: "city", type: "text" },    // Becomes "address.city"
  ],
}

Variants

Apply different styles based on context:

// Register variant-specific components
const components = {
  text: DefaultInput,
  compact: {
    text: CompactInput,
  },
};

// Use variant on the whole form
<FormGenerator schema={schema} variant="compact" />

// Or per-section
{ variant: "compact", fields: [...] }

// Or per-field
{ name: "notes", type: "text", variant: "compact" }

Dynamic Options Loading

{
  name: "city",
  type: "select",
  watchNames: ["country"],
  loadOptions: async (values) => {
    const cities = await fetchCities(values.country);
    return cities.map(c => ({ label: c.name, value: c.id }));
  },
  debounceMs: 300,
}

Custom Section Render

{
  title: "Payment",
  render: ({ control, disabled }) => (
    <StripeElements>
      <CardElement />
      <FormInput name="billingName" control={control} />
    </StripeElements>
  ),
}

Custom Field Render

{
  name: "avatar",
  type: "file",
  render: ({ field, control, error, fieldId }) => (
    <AvatarUploader fieldId={fieldId} error={error} />
  ),
}

Custom Props

Pass arbitrary props to your field components via customProps:

{
  name: "bio",
  type: "textarea",
  label: "Biography",
  customProps: {
    maxCharacters: 500,
    showCounter: true,
  },
}

Access in your component:

function FormTextarea({ field, customProps, ...props }: FieldComponentProps) {
  const maxChars = customProps?.maxCharacters as number;
  // ...
}

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" },
      ],
    },
  ],
}

Schema Builder Utilities

Type-safe helpers for defining schemas outside of components:

import { defineSchema, defineField, defineSection } from "@classytic/formkit/server";

const emailField = defineField<MyFormData>({
  name: "email",
  type: "email",
  label: "Email Address",
  required: true,
});

const personalSection = defineSection<MyFormData>({
  title: "Personal Info",
  cols: 2,
  fields: [emailField],
});

const schema = defineSchema<MyFormData>({
  sections: [personalSection],
});

Type Exports

import type {
  // Core
  FormSchema,
  FormGeneratorProps,
  BaseField,
  Section,

  // Components
  FieldComponentProps,
  FieldComponent,
  ComponentRegistry,

  // Layouts
  SectionLayoutProps,
  GridLayoutProps,
  LayoutComponent,
  LayoutRegistry,

  // Options
  FieldOption,
  FieldOptionGroup,

  // Conditions
  ConditionRule,
  ConditionConfig,
  Condition,

  // Hook types
  UseFormKitOptions,
  UseFormKitReturn,

  // Utility types
  FieldType,
  LayoutType,
  Variant,
  DefineField,
  InferSchemaValues,
  SchemaFieldNames,
  FormElement,
} from "@classytic/formkit";

Browser Support

  • React 19.0+
  • All modern browsers

License

MIT © Classytic

Keywords

react

FAQs

Package last updated on 11 Mar 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts