ZodsForm 🚀
TypeScript library to create dynamic forms with automatic validation using Zod
ZodsForm allows you to create complete HTML forms from Zod schemas or JSON structures, with real-time validation, and a simple, intuitive API.

✨ Features
- 🎯 Auto-generation from Zod: Convert Zod schemas into functional forms
- ✅ Real-time validation: Validation while user types and on blur
- 🔄 Nested field support: Objects and complex structures with dot-notation
- � Array support: Dynamic arrays with add/remove controls and element-level validation
- �📦 Native TypeScript: Full typing and autocomplete
- 🎭 Input masks: Automatic formatting (phone, credit card, ZIP, etc.) with string or regex
- 🔐 Password fields: With automatic show/hide toggle
- 🎨 Custom components: 3-level system (global, instance, field)
- 🎛️ Dynamic properties: Change visibility, size, options in real-time
- 🌐 Multiple input types: text, number, email, password, textarea, select, checkbox
- 🎯 Auto-mounting: Specify the element where the form will be mounted
- 🔍 Cross-field validation: Support for Zod's
.refine() (e.g., confirm password)
📦 Installation
npm install zodsform zod
Or with yarn:
yarn add zodsform zod
Note: ZodsForm uses pure CSS (no CSS frameworks required). Just import the styles:
import 'zodsform/dist/styles.css'
Or in HTML:
<link rel="stylesheet" href="node_modules/zodsform/dist/styles.css">
No additional dependencies or build tools needed! The CSS file is ready to use out of the box.
🚀 Quick Start
1. From a Zod Schema with Extensions (Recommended)
import { ZodsForm, z } from "zodsform";
import "zodsform/dist/styles.css";
const userSchema = z.object({
firstName: z.string()
.min(2, "Minimum 2 characters")
.label("First Name")
.properties({ size: 6 }),
lastName: z.string()
.min(2, "Minimum 2 characters")
.label("Last Name")
.properties({ size: 6 }),
email: z.string()
.email("Invalid email")
.label("Email Address"),
age: z.number()
.min(18, "Must be 18 or older")
.label("Age")
.properties({ visible: false }),
});
const form = ZodsForm.fromSchema(userSchema, {
el: "#root",
onSubmit: ({ data }) => {
console.log("Validated data:", data);
},
onChange: ({ fieldPath, data, errors, arrayIndex }) => {
console.log("Field changed:", fieldPath);
console.log("Array index:", arrayIndex);
console.log("Current data:", data);
console.log("Errors:", errors);
},
onValidate: ({ isValid, data, errors }) => {
console.log("Form valid:", isValid);
}
});
form.render();
2. With Input Masks
const contactSchema = z.object({
phone: z.string()
.length(10, "Phone must have 10 digits")
.label("Phone")
.properties({ mask: "###-###-####" }),
zipCode: z.string()
.length(5, "Invalid ZIP code")
.label("ZIP Code")
.properties({ mask: "#####" }),
creditCard: z.string()
.length(16, "Invalid card")
.label("Credit Card")
.properties({ mask: "####-####-####-####" }),
customMask: z.string()
.label("Only numbers 1-6")
.properties({ mask: /^[1-6]\d{0,5}$/ }),
});
3. With Password Fields
const securitySchema = z.object({
security: z.object({
password: z.string()
.min(8, "Minimum 8 characters")
.label("Password")
.password(),
confirmPassword: z.string()
.label("Confirm Password")
.password(),
}).label("Security"),
})
.refine((data) => data.security.password === data.security.confirmPassword, {
message: "Passwords do not match",
path: ["security.confirmPassword"],
});
📚 Zod Extensions
ZodsForm extends Zod with chainable methods to configure fields:
.label(string) | Defines the label text | z.string().label("Full Name") |
.properties({...}) | Configures field properties | z.string().properties({ size: 6, mask: "###-####" }) |
.password(boolean) | Converts to password field with toggle | z.string().password() |
.component(ComponentConfig) | Assigns a custom component | z.boolean().component(ToggleSwitch) |
Available Properties
.properties({
size?: number;
visible?: boolean;
disabled?: boolean;
placeholder?: string;
mask?: string | RegExp;
options?: Array<{
value: string | number;
label: string;
}>;
})
📚 Supported Field Types
text | Text input | z.string() |
number | Numeric input | z.number() |
email | Email input | z.string().email() |
password | Password input with toggle | z.string().password() |
textarea | Text area | z.string() (long) |
select | Dropdown selector | z.enum(["a", "b"]) or z.enum({ a: "A", b: "B" }) |
checkbox | Checkbox | z.boolean() |
array | Dynamic array with controls | z.array(z.object({...})) |
section | Visual container | - |
box | Bordered container | z.object({...}).label("Title") |
🔥 Advanced Examples
Form with Nested Objects
const addressSchema = z.object({
user: z.object({
name: z.string().label("Name"),
email: z.string().email().label("Email"),
}).label("User"),
address: z.object({
street: z.string().label("Street"),
city: z.string().label("City"),
zipCode: z.string()
.length(5)
.label("ZIP Code")
.properties({ mask: "#####" }),
}).label("Address"),
});
const form = ZodsForm.fromSchema(addressSchema, {
onSubmit: (data) => {
console.log(data);
},
});
Form with Select (Enum)
const simpleEnum = z.object({
role: z.enum(["admin", "user", "guest"]).label("Role"),
});
const customEnum = z.object({
country: z.enum({
mx: "Mexico",
us: "United States",
es: "Spain",
ar: "Argentina"
}).label("Country"),
});
const form = ZodsForm.fromSchema(customEnum);
Form with Custom Validations
const schema = z.object({
username: z
.string()
.min(3, "Minimum 3 characters")
.max(20, "Maximum 20 characters")
.regex(/^[a-zA-Z0-9_]+$/, "Only letters, numbers and underscore"),
age: z
.number()
.min(18, "Must be 18 or older")
.max(100, "Invalid age"),
website: z
.string()
.url("Invalid URL")
.optional(),
});
Form with Arrays
const securitySchema = z.object({
security: z.array(
z.object({
password: z.string()
.min(8, "Minimum 8 characters")
.label("Password")
.password(),
confirmPassword: z.string()
.label("Confirm Password")
.password(),
})
).label("Security Credentials")
.min(1, "At least one set of credentials is required"),
})
.refine((data) => {
return data.security.every(item => item.password === item.confirmPassword);
}, {
message: "Passwords do not match",
path: ["security", 0, "confirmPassword"],
});
const form = ZodsForm.fromSchema(securitySchema, {
onChange: ({ fieldPath, data, arrayIndex }) => {
if (arrayIndex !== undefined) {
console.log(`Field ${fieldPath} changed in array element ${arrayIndex}`);
}
},
onSubmit: ({ data }) => {
console.log(data);
}
});
Array Features:
- ➕ Add button: "+ Agregar" button to add new elements
- ✕ Remove button: Each element has a remove button
- 🛡️ Smart deletion: If only one element remains, it clears the data instead of removing the element
- ✅ Independent validation: Each array element validates independently
- 🎯 Element tracking:
arrayIndex parameter in onChange callback to identify which element changed
- 🔍 Cross-field validation:
refine() works correctly with array elements
🎛️ API
ZodsForm.fromSchema(schema, config)
Creates a form from a Zod schema.
Parameters:
schema: Zod schema (ZodObject)
config:
el?: CSS selector (string) or DOM element where to mount the form
onValidate?: Callback when the form is validated ({ isValid, data, errors }) => void
onSubmit?: Callback when the form is submitted ({ data }) => void
onChange?: Callback on each field change ({ fieldPath, data, errors, arrayIndex }) => void
fieldPath has strong typing based on schema structure (includes array element properties!)
arrayIndex? is a number indicating which array element changed (undefined for non-array fields)
components?: Custom components map
Returns: ZodsForm instance
Example:
const form = ZodsForm.fromSchema(mySchema, {
el: "#app",
onSubmit: ({ data }) => console.log(data),
onChange: ({ fieldPath, data, errors, arrayIndex }) => {
console.log('Changed:', fieldPath);
if (arrayIndex !== undefined) {
console.log('Array element index:', arrayIndex);
}
}
});
form.render();
new ZodsForm(config, el?)
Creates a form from a JSON structure.
Parameters:
config.structure: Form structure
config.schema?: Optional Zod schema for full validation
config.onSubmit?: Submit callback ({ data }) => void
config.onChange?: Change callback ({ fieldPath, data, errors, arrayIndex }) => void
config.onValidate?: Validate callback ({ isValid, data, errors }) => void
config.components?: Custom components map
el?: CSS selector or DOM element (optional)
Instance Methods
form.render(): HTMLElement
Renders the form and returns the DOM element.
form.getData(): any
Gets the current form data.
form.getErrors(): any
Gets the current validation errors.
form.setData(data: Record<string, any>): void
Sets values in the form programmatically.
form.setData({
firstName: "John",
email: "john@example.com",
age: 25,
});
form.setFieldProperty(fieldPath, property, value): void
Dynamically changes any field property in the form in real-time. Supports nested fields with dot-notation.
form.setFieldProperty("companyName", "visible", false);
form.setFieldProperty("address.street", "visible", true);
form.setFieldProperty("email", "size", 12);
form.setFieldProperty("zipCode", "disabled", true);
form.setFieldProperty("country", "options", [
{ value: "mx", label: "Mexico" },
{ value: "us", label: "USA" }
]);
form.setFieldProperty("age", "min", 18);
form.setFieldProperty("email", "className", "bg-yellow-100");
form.setFieldProperty("phone", "mask", "###-###-####");
Supported properties: visible, size, disabled, className, min, max, options, mask
🎭 Mask System
ZodsForm supports two types of masks for data input:
1. Format Masks (String)
Apply automatic formatting while the user types. The # symbol represents a digit:
const maskedSchema = z.object({
phone: z.string().properties({ mask: "###-###-####" }),
zipCode: z.string().properties({ mask: "#####" }),
creditCard: z.string().properties({ mask: "####-####-####-####" }),
});
Important: The value is stored WITHOUT formatting (digits only), but displayed formatted in the input.
2. Validation Masks (RegExp)
Validate each character in real-time according to a regular expression:
const regexMaskSchema = z.object({
customCode: z.string().properties({
mask: /^[1-6]\d{0,5}$/
}),
alphanumeric: z.string().properties({
mask: /^[a-zA-Z0-9]*$/
}),
});
🔧 Grid System
ZodsForm uses a 12-column grid system based on CSS Grid. Use the size property to control width:
const gridSchema = z.object({
firstName: z.string()
.label("First Name")
.properties({ size: 6 }),
lastName: z.string()
.label("Last Name")
.properties({ size: 6 }),
email: z.string()
.label("Email")
.properties({ size: 12 }),
});
Common sizes:
size: 3 → 25% (3/12)
size: 4 → 33% (4/12)
size: 6 → 50% (6/12)
size: 8 → 66% (8/12)
size: 12 → 100% (12/12) - default for boxes and textareas
🎨 Custom Components
ZodsForm allows you to completely customize the appearance of your forms with three levels of customization in priority order:
1. Field-level Components (Zod) - Highest Priority
const schema = z.object({
notifications: z.boolean()
.label("Notifications")
.component(ToggleSwitch),
});
2. Instance-level Components
const form = ZodsForm.fromSchema(schema, {
components: {
boolean: MyCustomToggle,
acceptTerms: StyledCheckbox,
},
onSubmit: (data) => console.log(data)
});
3. Global Components
import { ZodsForm } from "zodsform";
ZodsForm.registerComponents({
boolean: ToggleSwitch,
});
Creating Custom Components
import type { ComponentConfig } from "zodsform";
export const ToggleSwitch: ComponentConfig = {
render: (config) => {
const wrapper = document.createElement('div');
const input = document.createElement('input');
input.type = 'checkbox';
input.name = config.fieldPath;
wrapper.appendChild(input);
return wrapper;
},
getValue: (element) => {
return element.querySelector('input')?.checked || false;
},
setValue: (element, value) => {
const input = element.querySelector('input');
if (input) input.checked = Boolean(value);
}
};
🤝 Contributing
Contributions are welcome! Please:
- Fork the project
- Create a feature branch (
git checkout -b feature/AmazingFeature)
- Commit your changes (
git commit -m 'Add some AmazingFeature')
- Push to the branch (
git push origin feature/AmazingFeature)
- Open a Pull Request
🔄 Migration from v0.1.x
If you were using previous versions, here are the main changes:
- ✅ Nested fields fixed: Validation of fields within objects now works correctly
- ✅ Improved masks: Support for regex in addition to string patterns
- ✅ Improved components: Clearer 3-level priority system
- ⚠️ Removed methods:
labels in config no longer exists, use .label() in schema
❓ FAQ
Los estilos no se muestran después de instalar el paquete
Solución: Asegúrate de importar el CSS en tu proyecto:
import 'zodsform/dist/styles.css';
O en HTML:
<link rel="stylesheet" href="node_modules/zodsform/dist/styles.css">
El CSS está listo para usar sin necesidad de herramientas de build adicionales.
¿Necesito instalar algún framework CSS?
No. ZodsForm usa CSS puro sin dependencias externas. Todo está incluido en el archivo dist/styles.css. Solo necesitas instalar zodsform y zod.
¿Puedo personalizar los estilos?
Sí, de tres formas:
- CSS personalizado: Sobreescribe las clases de ZodsForm en tu propio CSS
- Componentes personalizados: Crea tus propios componentes con tu lógica de UI
- Propiedades inline: Usa
className en .properties() para agregar clases personalizadas
� Additional Documentation
👤 Author
Dinnger
🙏 Acknowledgments
- Zod - TypeScript-first validation library
- The TypeScript community
📝 License
ISC License - see the LICENSE file for more details
Made with ❤️ by Dinnger