@classytic/formkit
v1.7.0
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.
Features
- Headless - Bring your own UI components (Shadcn, MUI, Chakra, etc.) via a
one-time component/layout adapter. Already using shadcn? Skip the adapter
entirely with the prebuilt one in
@classytic/fluid/formkit— itsSchemaFormis genuinely a schema +onSubmitaway. - Minimal boilerplate - once an adapter is registered,
useFormKitwires a full form (defaults + RHF + generator props) in a few lines - 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/serverentry 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, andfieldStateprops - Validation helpers -
buildValidationRulesgenerates RHF rules from schema props - Lightweight - ~17KB gzipped (peer deps excluded, everything included), 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-formQuick 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,
name,
rules, // pre-computed RHF rules from schema (required, minLength, pattern, validate…)
label,
placeholder,
required,
fieldId, // use as id on <input> and htmlFor on <Label>
errorId, // use as id on error <p> and aria-errormessage on <input>
shouldShowError, // true only after touch/submit — mirrors :user-invalid timing
error,
}: FieldComponentProps) {
return (
<Controller
name={name}
control={control}
rules={rules}
render={({ field }) => (
<div className="space-y-2">
{label && (
<Label htmlFor={fieldId}>
{label}
{required && <span aria-hidden="true" className="text-red-500 ml-1">*</span>}
</Label>
)}
<Input
{...field}
id={fieldId}
placeholder={placeholder}
aria-required={required || undefined}
aria-invalid={shouldShowError || undefined}
aria-errormessage={shouldShowError ? errorId : undefined}
/>
{shouldShowError && (
<p id={errorId} role="alert" 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 }) => {
// Map to STATIC class names. A template like `grid-cols-${cols}` is never
// generated by Tailwind's JIT (it only sees whole class strings in source).
const COLS: Record<number, string> = {
1: "grid-cols-1",
2: "grid-cols-1 md:grid-cols-2",
3: "grid-cols-1 md:grid-cols-2 lg:grid-cols-3",
};
return <div className={`grid gap-4 ${COLS[cols] ?? "grid-cols-1"}`}>{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>
);
}Type safety
Field names are enforced against your form type at authoring time by the
builders. Use field.for<T>(), defineField<T>(), or section<T>() and a typo
is a compile error:
const f = field.for<SignupData>();
f.text("emial", "Email"); // ✗ compile error — "emial" is not a key of SignupData
f.text("email", "Email"); // ✓The <FormGenerator schema> / useFormKit({ schema }) props themselves accept
FormSchema<any> by design: a schema's condition functions ((values) => …)
make FormSchema<T> contravariant in T, so pinning the prop to the form's T
would force a cast at every call site. Author with the typed builders for name
safety; the generator stays permissive so any built schema drops in without
ceremony. itemFields (group/array children) use relative names and are
resolved at render time, so they are typed loosely on purpose.
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
resetOnSchemaChange: true, // optional — re-seed on schema swap
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). Nested/namespaced defaults are extracted as a nested object ({ address: { city } }), so react-hook-form's dot-path resolution applies them correctly.
Pass resetOnSchemaChange: true to re-seed the form when the schema identity changes at runtime (e.g. a schema loaded from a DB is swapped in). It's off by default because a reset discards in-progress edits.
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 accepts Path<T> or any string.
// Use field.for<T>() builders for call-site enforcement of valid paths.
// Relative names are correct for nameSpace sections and itemFields children —
// FormGenerator prefixes them at render time (e.g. "street" → "address.street").
name: Path<T> | 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 — reacts to OTHER FIELDS (country → cities)
loadOptions?: (formValues: Partial<T>) => Promise<FieldOption[]> | FieldOption[];
// Async options — reacts to WHAT THE USER TYPES (asyncCombobox/asyncMultiselect).
// The adapter owns debounce + loading UI; forward the signal to cancel a
// superseded keystroke.
onSearch?: (
query: string,
options?: { signal: AbortSignal },
) => Promise<(FieldOption | FieldOptionGroup)[]>;
debounceMs?: number;
// Sub-fields for group and array types.
// Children use relative names ("street") — FormGenerator prefixes with parent name.
// Intentionally untyped to T because they resolve at render time, not authoring time.
itemFields?: BaseField[];
// 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;
isSubmitted: boolean;
error?: FieldError;
};
fieldId: string; // Generated ID for label-input association (e.g. "formkit-field-email")
errorId: string; // ID for the error node — set as aria-errormessage on the input
shouldShowError: boolean; // true only after touch/submit (mirrors :user-invalid timing)
rules: RegisterOptions; // Pre-computed RHF rules from schema (required/min/max/pattern…)
isLoading?: boolean; // true while async loadOptions is in flight
}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.
Validation & helpers
Standard Schema — bring your own validator
Pass any Standard Schema (zod, valibot, arktype,
or your own) as validationSchema and FormKit builds the resolver. There is no
@hookform/resolvers/* package to install and no per-library adapter.
import { useFormKit } from "@classytic/formkit";
import { z } from "zod";
const validationSchema = z.object({
email: z.string().email(),
age: z.coerce.number().min(18),
});
// Both types are INFERRED from validationSchema — no generic to declare.
const form = useFormKit({ schema, validationSchema });
form.watch("email"); // string
form.watch("nope"); // ✖ compile errorForm state is typed off the schema's input; handleSubmit off its
output. This matters the moment a schema coerces. An <input type="number">
puts a string in form state, and z.coerce.number() turns it into a number
when validation runs — on submit, not before:
const validationSchema = z.object({ age: z.coerce.number().min(18) });
const form = useFormKit({ schema, validationSchema });
form.watch("age"); // string — what the input holds now
form.setValue("age", "18"); // string — ditto
form.handleSubmit((values) => values.age); // number — parsed outputTyping watch off the output would let form.watch("age").toFixed() compile
against a value that is still "18" at runtime. RHF models this with separate
input and transformed-output generics, and FormKit keeps them separate.
For a schema that does not transform, input and output coincide and there is nothing to think about.
An explicit resolver still wins, so existing zodResolver(...) call sites are
untouched.
Field rules and the schema are complementary, not alternatives:
| | Field rules (required, pattern, …) | validationSchema |
|---|---|---|
| Runs | As you type / on blur | On submit |
| Drives | Native constraint attributes → :user-invalid | Parsing, transforms, cross-field |
| Works without JS | Yes | No (server action: yes) |
standardResolver, validateStandardSchema, isStandardSchema and
issuePathToName are exported if you want the pieces directly.
Native constraints & :user-invalid
buildConstraintAttributes(field) gives the same constraints as HTML
attributes, so the browser validates before hydration and you get deferred
error styling in pure CSS. Adapters receive it as constraints:
function TextInput({ field, constraints, shouldShowError, errorId }: FieldComponentProps) {
return (
<input
{...constraints} // type, required, minLength, pattern, inputMode…
aria-invalid={shouldShowError || undefined}
aria-errormessage={shouldShowError ? errorId : undefined}
/>
);
}/* Only after the user commits — not while they are still typing. */
input:user-invalid { border-color: var(--error); }
input:user-valid { border-color: var(--success); }It deliberately omits required on checkbox groups and multiselects (there the
attribute means "this specific box", not "the group"), and omits min/max on
text inputs where the browser would ignore them.
Server actions (React 19)
The same schema that renders the form decodes the FormData, and the same
validator runs on the server:
// app/actions.ts
"use server";
import { validateFormData, type FormActionState } from "@classytic/formkit/server";
export async function saveUser(_prev: FormActionState, formData: FormData) {
const result = await validateFormData(schema, validationSchema, formData);
if (!result.success) return { errors: result.errors };
await db.users.create(result.value); // parsed + transformed
return { errors: {} };
}const [state, action, pending] = useActionState(saveUser, { errors: {} });
<form action={action}>…</form>formDataToValues walks the schema, not the FormData — so an unchecked
checkbox becomes false instead of vanishing, and keys an attacker appended to
the request are ignored because they were never declared. Path segments named
__proto__ / constructor / prototype are refused outright.
Array rows are compacted onto dense 0..n-1 positions. The browser submits
items.0 and items.2 after the user deletes the middle row; writing those
verbatim leaves a hole that z.array(z.object(...)) rejects. It also means a
submitted index is a position, not an address — items.4294967294.sku is just
row 0, so no request can make the server allocate for it.
maxArrayRows therefore bounds a row count, not an index. That is a product
decision, so raise it when a form legitimately posts more:
await validateFormData(schema, validationSchema, formData, { maxArrayRows: 50_000 });validateSchema
Structurally validate a schema (great for DB-loaded / resetOnSchemaChange
schemas). Server-safe; returns SchemaIssue[] (empty ⇒ OK). FormGenerator
also runs it automatically in dev and console.warns any issues.
import { validateSchema } from "@classytic/formkit"; // or /server
for (const issue of validateSchema(schema)) {
console.warn(`${issue.severity} ${issue.path}: ${issue.message}`);
}
// Detects: missing name/type, duplicate names (namespace-aware),
// itemFields on a non-container, empty containers, unknown DSL operators.It validates shape, not your component registry — an unregistered field
type is a rendering concern, surfaced separately (see onMissingComponent).
focusFirstError
Focus + scroll to the first visibly-invalid field, in layout order. Wire it into the submit's invalid handler:
import { focusFirstError } from "@classytic/formkit";
<form onSubmit={handleSubmit(onValid, () => focusFirstError())}>React Hook Form calls the invalid handler before React commits the re-render
that marks fields invalid, so if the immediate query finds nothing,
focusFirstError automatically retries on the next two animation frames — the
plain wiring above works on the very first submit. It returns true when a
field was focused immediately, false when the attempt was deferred.
onMissingComponent
If a field's type has no registered component, the form renders a visible
placeholder (dev and prod — never a silent drop or a wrong-widget text input)
and notifies this callback so you can log the gap. hidden fields are the one
exception: with no registered component they render nothing by design (the
value still lives in form state); register a hidden component to override.
<FormSystemProvider
components={components}
layouts={layouts}
onMissingComponent={(type) => reportToTelemetry("formkit_missing_component", { type })}
>Security note:
buildValidationRulescompiles a field'spatterninto anew RegExp(...). If your schemas are ever authored by untrusted users, treatpatternas a ReDoS surface — validate/limit it before persisting.
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.
Passing a schema across the RSC → Client boundary
FormGenerator is a Client Component (it drives react-hook-form). If you build
a schema in a Server Component and pass it as a prop into the client, React
requires that prop to be serializable — so function members won't cross
the boundary:
- ❌ function
condition: (values) => …, customrender, andloadOptions(functions aren't serializable) - ✅ DSL conditions (
{ watch, operator, value }), and all the plain data fields (name,type,label,options,required,min/max, …)
For fully static, server-defined schemas use DSL conditions. If a schema needs
function conditions / custom renders / async options, define it inside the
Client Component (e.g. the file that renders FormGenerator) rather than
passing it down from an RSC. This is a general Next.js constraint, not specific
to formkit.
Advanced Features
Conditional Fields (Function)
{
name: "companyName",
type: "text",
label: "Company Name",
condition: (values) => values.accountType === "business",
}Performance: a function condition can't be statically analyzed, so the field subscribes to the whole form and re-evaluates on every change. For large forms prefer a DSL rule (below, whose watched paths are auto-scoped), or add
watchNamesto scope the subscription yourself:{ condition: (v) => v.accountType === "business", watchNames: ["accountType"] }.
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, { signal }) => {
// Forward `signal` so a superseded/unmounted load is cancelled.
const cities = await fetchCities(values.country, { signal });
return cities.map(c => ({ label: c.name, value: c.id }));
},
debounceMs: 300,
cacheOptions: true, // reuse the last fetch when a dependency is re-selected
}
cacheOptions(defaultfalse) memoizes results per watched-value signature for the field's lifetime, so togglingcountryback and forth doesn't re-hit the API. Leave it off when the options can change server-side between loads.
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
Trademark
MIT-licensed code. "Classytic"/"arc" names + logos are trademarks of Classytic LLC — see TRADEMARK.md.
