SmartForm
ViewA type-safe React form package with schema-driven fields, built-in validation, and an escape hatch for completely custom layouts.
The problem: form code kept repeating itself
Most product forms repeat the same plumbing: field state, labels, validation, error messages, disabled states, and submit handling. SmartForm started as an attempt to collapse that work into one typed definition without locking the interface into one layout.
I designed the package around a simple constraint: the schema should describe the form, but the consumer should still own the experience.
A schema becomes the source of truth
Each field lives in a typed schema with its input type, label, default value, and validation rules. SmartForm reads that schema to render the default interface and infer the shape of the submitted values.
npm install @cermuel/smart-formimport { SmartForm } from '@cermuel/smart-form';
import '@cermuel/smart-form/dist/styles.css';
const loginSchema = {
email: {
type: 'email',
label: 'Email address',
required: true,
},
password: {
type: 'password',
label: 'Password',
required: true,
min: 8,
},
remember: {
type: 'checkbox',
checkboxLabel: 'Remember me',
},
} as const;
const LoginForm = () => (
<SmartForm
schema={loginSchema}
onSubmit={(values) => console.log(values)}
buttonTitle="Sign in"
/>
);The package covers text, email, number, password, textarea, checkbox, select, date, and time fields. That was enough breadth for common product flows while keeping the API small.
Validation stays beside the field
Required state, numeric boundaries, and regular-expression patterns are declared next to the field they protect. The form validates from that same schema, so the rules do not drift away from the UI or the submission type.
const profileSchema = {
age: {
type: 'number',
label: 'Age',
required: true,
min: 18,
max: 100,
},
website: {
type: 'string',
label: 'Website',
pattern: /^https?:\/\/.+/,
},
} as const;TypeScript carries the schema into the submit callback. Text-like fields are inferred as strings and checkboxes as booleans, so consumers do not have to maintain a second handwritten values interface.
The default UI is a starting point
SmartForm ships Tailwind styling, field labels, error presentation, a password visibility control, and submit loading and disabled states. Consumers can extend the default interface through focused class props instead of rebuilding the form for small visual changes.
<SmartForm
schema={loginSchema}
onSubmit={handleLogin}
formClassname="max-w-md mx-auto"
inputClassname="shadow-sm"
buttonTitle="Continue"
buttonLoading={isSubmitting}
buttonDisabled={isSubmitting}
/>An escape hatch for custom layouts
A form library becomes a constraint when its rendering decisions cannot be replaced. The render function exposes current values, errors, and a typed setter, while SmartForm continues to own validation and submission.
<SmartForm schema={loginSchema} onSubmit={handleLogin}>
{({ values, setValue, errors }) => (
<div className="space-y-2">
<label htmlFor="email">Email address</label>
<input
id="email"
value={values.email}
onChange={(event) =>
setValue('email', event.target.value)
}
/>
{errors.email ? <p>{errors.email}</p> : null}
</div>
)}
</SmartForm>This keeps the package useful for quick forms and fully designed product surfaces. The consumer can begin with the generated interface, customize individual classes, or take over the complete layout without losing the schema-driven lifecycle.
Shipping the package
I published SmartForm as an MIT-licensed npm package with built-in TypeScript declarations and no runtime dependencies. Keeping the dependency graph at zero made the component easier to adopt and reduced the chance of version conflicts inside host applications.
The project taught me that a useful abstraction should remove repetition while leaving a clear path back to direct control. SmartForm handles the predictable form mechanics; the product team keeps ownership of the interface.
