npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@classytic/formkit

v1.3.1

Published

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

Readme

@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: "[email protected]" }, // 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

Links