julianabot/shadcn-form-designer(opens in a new tab)
Language: TypeScript
Component library for building and rendering visual forms with ShadCN UI and Zod schema support.
- stars
- 31
- watchers
- 2
- forks
- 5
- open issues
- 0
- Last updated
- 04/07/2026
Language: TypeScript
Component library for building and rendering visual forms with ShadCN UI and Zod schema support.
A dynamic form builder and renderer for React. Define forms as JSON, render them with Zod validation, and let users design forms visually — built on ShadCN UI, Tailwind CSS, and react-hook-form.
npm install @julianabot/shadcn-form-designer
Peer dependencies:
npm install react react-dom react-hook-form @hookform/resolvers zod
Initialize the built-in field types once at your app's entry point, then render forms from JSON config.
import { registerBuiltInFields, DynamicForm, } from "@julianabot/shadcn-form-designer"; import type { SerializableFieldConfig } from "@julianabot/shadcn-form-designer"; // Call once before rendering registerBuiltInFields(); const fields: SerializableFieldConfig[] = [ { type: "input", name: "fullName", label: "Full Name", required: true, minLength: 3, maxLength: 50, validation: { type: "string", min: 3, max: 50 }, }, { type: "select", name: "role", label: "Role", options: [ { label: "Developer", value: "dev" }, { label: "Designer", value: "design" }, ], validation: { type: "enum", values: ["dev", "design"] }, }, { type: "switch", name: "newsletter", label: "Subscribe to newsletter", validation: { type: "boolean" }, }, ]; function App() { return <DynamicForm config={fields} onSubmit={(data) => console.log(data)} />; }
| Type | Description | Config |
|---|---|---|
input |
Text input | minLength, maxLength, placeholder |
textarea |
Multi-line text | minLength, maxLength, placeholder |
select |
Dropdown select | options: { label, value }[] |
combobox |
Searchable select | options: { label, value }[] |
radio |
Radio group | options: { label, value }[] |
date |
Date picker | minDate, maxDate |
file |
File upload | accept, maxSizeMB |
switch |
Boolean toggle | — |
time |
Time picker | placeholder |
DynamicForm accepts three config formats. It auto-detects which one you pass.
A SerializableFieldConfig[] with JSON-serializable validation descriptors. Good for storing in a database.
const fields: SerializableFieldConfig[] = [ { type: "input", name: "email", label: "Email", required: true, validation: { type: "string", pattern: "email" }, }, ];
Groups fields into ordered sections with titles and descriptions.
const layout: FormLayout = { fields: { fullName: { type: "input", name: "fullName", label: "Full Name", required: true, validation: { type: "string", min: 3, max: 50 }, }, country: { type: "select", name: "country", label: "Country", options: [ { label: "Philippines", value: "PH" }, { label: "United States", value: "US" }, ], validation: { type: "enum", values: ["PH", "US"] }, }, }, sections: [ { order: 0, title: "Personal Info", fields: [ { fieldName: "fullName", order: 0 }, { fieldName: "country", order: 1 }, ], }, ], };
A FieldWithValidation<ZodTypeAny>[] with live Zod schemas, for when you need full control over validation. Use buildValidation() to create these from FieldConfig objects.
| Prop | Type | Description |
|---|---|---|
config |
SerializableFieldConfig[] | FormLayout | FieldWithValidation[] |
Form definition (auto-detected) |
mode |
"view" | "editor" |
"view" renders a normal form (default). "editor" shows a sortable field list with edit, delete, and drag-to-reorder. |
onSubmit |
(data: Record<string, unknown>) => void |
Called with validated form data on submit |
onSubmitFieldConfig |
(fields: FieldWithValidation[]) => void |
Called with hydrated field configs on submit — useful for builder/designer mode |
onChange |
(values: Record<string, unknown>) => void |
Called on every value change with current form state |
onUnsupportedField |
(type: string, fieldName: string) => void |
Called when a field type has no registered definition. Defaults to console.warn |
onFieldClick |
(fieldName: string) => void |
Editor mode: called when the edit button is clicked |
onFieldDelete |
(fieldName: string) => void |
Editor mode: called when the delete button is clicked |
onFieldsReorder |
(orderedNames: string[]) => void |
Editor mode: called after drag-and-drop with the new ordered field names |
AddFieldDialog gives users a UI to add fields at runtime. It also supports an edit mode — pass initialValues with controlled open/onOpenChange to reopen the dialog prepopulated with an existing field's config.
| Prop | Type | Description |
|---|---|---|
handleAddField |
(values: FieldConfig) => void |
Called with the new/updated field config on submit |
initialValues |
AddFieldFormValues |
Prepopulates the dialog for editing an existing field |
open |
boolean |
Controls open state externally (for edit mode) |
onOpenChange |
(open: boolean) => void |
Called when the dialog wants to close (for edit mode) |
When initialValues is provided, the dialog shows "Edit Field" / "Save Changes" instead of "Add New Field" / "Submit".
Set mode="editor" on DynamicForm to get a field management UI instead of a live form. Each field renders as a row with:
@dnd-kit/sortable)AddFieldDialog prepopulated with the field's configimport { registerBuiltInFields, AddFieldDialog, DynamicForm, toSerializableField, toFormLayout, toAddFieldValues, } from "@julianabot/shadcn-form-designer"; import type { FieldConfig, SerializableFieldConfig, } from "@julianabot/shadcn-form-designer"; import { useState, useCallback } from "react"; registerBuiltInFields(); function FormDesigner() { const [fields, setFields] = useState<SerializableFieldConfig[]>([]); const [editingFieldName, setEditingFieldName] = useState<string | null>(null); const handleAddField = (config: FieldConfig) => { setFields((prev) => [...prev, toSerializableField(config)]); }; const handleEditField = (config: FieldConfig) => { if (!editingFieldName) return; setFields((prev) => prev.map((f) => f.name === editingFieldName ? toSerializableField(config, editingFieldName) : f, ), ); setEditingFieldName(null); }; const editingField = editingFieldName ? fields.find((f) => f.name === editingFieldName) : null; return ( <div> <AddFieldDialog handleAddField={handleAddField} /> <DynamicForm config={toFormLayout(fields)} mode="editor" onFieldClick={(name) => setEditingFieldName(name)} onFieldDelete={(name) => setFields((prev) => prev.filter((f) => f.name !== name)) } onFieldsReorder={(ordered) => setFields((prev) => { const map = new Map(prev.map((f) => [f.name, f])); return ordered.map((n) => map.get(n)!); }) } /> {/* Edit dialog — controlled externally */} <AddFieldDialog handleAddField={handleEditField} initialValues={ editingField ? toAddFieldValues(editingField) : undefined } open={!!editingFieldName} onOpenChange={(open) => { if (!open) setEditingFieldName(null); }} /> </div> ); }
The serializable config format is designed to be stored as JSON — no Zod instances, no component references. Here's the full round-trip:
import { toFormLayout } from "@julianabot/shadcn-form-designer"; import type { SerializableFieldConfig } from "@julianabot/shadcn-form-designer"; // Save to your backend — it's plain JSON const handleSave = async (fields: SerializableFieldConfig[]) => { const layout = toFormLayout(fields); await fetch("/api/forms", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(layout), }); };
To load a saved form, just pass the JSON back to DynamicForm:
import { DynamicForm } from "@julianabot/shadcn-form-designer"; import type { FormLayout } from "@julianabot/shadcn-form-designer"; import { useEffect, useState } from "react"; function FormRenderer({ formId }: { formId: string }) { const [layout, setLayout] = useState<FormLayout | null>(null); useEffect(() => { fetch(`/api/forms/${formId}`) .then((res) => res.json()) .then(setLayout); }, [formId]); if (!layout) return <p>Loading...</p>; return ( <DynamicForm config={layout} onSubmit={(data) => console.log("Submitted:", data)} /> ); }
The FormLayout and SerializableFieldConfig types are pure JSON — save them in a database column, a file, or send them over an API. The library hydrates the Zod validation schemas at render time.
Register your own field types with the field registry. Each registration provides a renderer component, validation builder, and optionally a config panel for the AddFieldDialog.
import { registerField, InputField } from "@julianabot/shadcn-form-designer"; import { z } from "zod"; registerField({ type: "email", label: "Email", category: "text", renderer: InputField, // reuse built-in or provide your own component buildValidation: (config) => ({ ...config, name: crypto.randomUUID(), validation: config.required ? z.string().email("Invalid email").min(1, "Required") : z.string().email("Invalid email").optional(), }), });
Validation is defined as plain JSON — no Zod in your config files. The library hydrates these into Zod schemas at runtime.
| Type | Fields |
|---|---|
string |
min, max, pattern (email, url, uuid, regex), regex |
boolean |
— |
enum |
values: [string, ...string[]] |
date |
min (ISO string), max (ISO string) |
file |
accept, maxSizeMB |
| Function | Description |
|---|---|
toSerializableField(config, name?) |
Converts a FieldConfig to a JSON-serializable SerializableFieldConfig |
toFormLayout(configs) |
Converts a flat SerializableFieldConfig[] into a FormLayout with a single section |
toAddFieldValues(field) |
Converts a SerializableFieldConfig back to AddFieldFormValues for edit mode |
buildValidation(config) |
Creates a FieldWithValidation with a live Zod schema from a FieldConfig |
buildZodSchema(configs) |
Hydrates a SerializableFieldConfig[] into FieldWithValidation[] with Zod schemas |
hydrateValidation(descriptor, required?) |
Converts a JSON ValidationDescriptor into a Zod schema |
MIT