
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
@classytic/formkit
Advanced tools
Headless, type-safe form generation engine for React 19. Schema-driven with full TypeScript support.
Headless, type-safe form generation engine for React 19. Schema-driven with full TypeScript support.
useFormKit hook: 5 lines to set up a complete form@classytic/formkit/server entry point for RSCfieldId, error, and fieldState propsbuildValidationRules generates RHF rules from schema propsnpm install @classytic/formkit react-hook-form
# or
pnpm add @classytic/formkit react-hook-form
# or
yarn add @classytic/formkit react-hook-form
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>
)}
/>
);
}
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>
);
}
// 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>
);
}
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.
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)
/>
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;
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;
}
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>;
}
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")
}
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.
const components: ComponentRegistry = {
// Simple mapping
text: FormInput,
select: FormSelect,
// Variant-specific components
compact: {
text: CompactInput,
select: CompactSelect,
},
};
const layouts: LayoutRegistry = {
section: SectionLayout,
grid: GridLayout,
// Variant-specific layouts
compact: {
section: CompactSection,
},
};
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.
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.
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.
{
name: "companyName",
type: "text",
label: "Company Name",
condition: (values) => values.accountType === "business",
}
{
name: "stateField",
type: "select",
label: "State",
condition: { watch: "country", operator: "===", value: "US" },
watchNames: ["country"], // Optimizes re-renders
}
{
title: "Business Details",
condition: (values) => values.accountType === "business",
fields: [
{ name: "companyName", type: "text", label: "Company" },
{ name: "taxId", type: "text", label: "Tax ID" },
],
}
{
name: "taxField",
type: "text",
condition: {
rules: [
{ watch: "country", operator: "===", value: "US" },
{ watch: "country", operator: "===", value: "CA" },
],
logic: "or",
},
}
DSL rules resolve dot-notation paths for nested form values:
{
name: "zipCode",
type: "text",
condition: { watch: "address.country", operator: "===", value: "US" },
}
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"
],
}
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" }
{
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,
}
{
title: "Payment",
render: ({ control, disabled }) => (
<StripeElements>
<CardElement />
<FormInput name="billingName" control={control} />
</StripeElements>
),
}
{
name: "avatar",
type: "file",
render: ({ field, control, error, fieldId }) => (
<AvatarUploader fieldId={fieldId} error={error} />
),
}
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;
// ...
}
{
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-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],
});
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";
MIT © Classytic
FAQs
Headless, type-safe form generation engine for React 19. Schema-driven with full TypeScript support.
We found that @classytic/formkit demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 1 open source maintainer collaborating on the project.
Did you know?

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.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.