A headless, bring-your-own-components library for rendering JSON Forms with any React component library
npm install @apart-tech/jsonforms-kitA headless, bring-your-own-components library for rendering JSON Forms with any React component library. The library handles all the heavy lifting (renderer registration, tester logic, layout dispatching, i18n integration, conditional visibility) while allowing consumers to provide their own UI components.
- Headless: No CSS or styling dependencies - bring your own components
- Type-safe: Full TypeScript support with component contracts
- Flexible: Works with shadcn/ui, MUI, Chakra UI, or any component library
- Comprehensive: Supports all common form controls out of the box
- Extensible: Easy to add custom controls and testers
``bash`
npm install @apart-tech/jsonforms-kit @jsonforms/core @jsonforms/react
`tsx
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from '@/components/ui/select';
import { Checkbox } from '@/components/ui/checkbox';
const componentMap = {
// Required components
Input: ({ value, onChange, type, placeholder, disabled, className, ...props }) => (
type={type}
value={value || ''}
onChange={(e) => onChange(e.target.value)}
placeholder={placeholder}
disabled={disabled}
className={className}
{...props}
/>
),
Label: ({ children, className, htmlFor }) => (
),
// Optional components
Select: ({ value, onChange, options, placeholder, disabled, className }) => (
),
Checkbox: ({ checked, onChange, disabled, className }) => (
onCheckedChange={onChange}
disabled={disabled}
className={className}
/>
),
// Layout components
VerticalLayout: ({ children, className }) => (
HorizontalLayout: ({ children, className }) => (
Group: ({ label, children, className }) => (
// Error display
ErrorMessage: ({ message, className }) => (
{message}
RequiredIndicator: ({ className }) => (
*
),
};
`
`tsx
import { createRenderers } from '@apart-tech/jsonforms-kit';
const renderers = createRenderers({
components: componentMap,
classNames: {
field: 'space-y-1',
label: 'text-sm font-medium',
error: 'text-sm text-red-600 mt-1',
required: 'text-red-500 ml-1',
layouts: {
vertical: 'space-y-6',
horizontal: 'flex gap-4 items-end',
group: 'border rounded-lg p-4',
},
},
});
`
`tsx
import { useState } from 'react';
import { JsonForms } from '@jsonforms/react';
import { FormProvider } from '@apart-tech/jsonforms-kit';
function MyForm() {
const [data, setData] = useState({});
const schema = {
type: 'object',
properties: {
name: { type: 'string', title: 'Name' },
email: { type: 'string', format: 'email', title: 'Email' },
role: {
type: 'string',
title: 'Role',
enum: ['admin', 'user', 'guest'],
},
},
required: ['name', 'email'],
};
return (
data={data}
renderers={renderers}
onChange={({ data }) => setData(data || {})}
/>
);
}
`
Each component type has a defined prop interface:
`typescript`
interface InputProps {
value: string;
onChange: (value: string) => void;
disabled?: boolean;
placeholder?: string;
type?: 'text' | 'email' | 'password' | 'tel' | 'url' | 'number';
className?: string;
}
`typescript`
interface SelectProps {
value: string;
onChange: (value: string) => void;
options: Array<{ value: string; label: string }>;
placeholder?: string;
disabled?: boolean;
className?: string;
}
`typescript`
interface CheckboxProps {
checked: boolean;
onChange: (checked: boolean) => void;
disabled?: boolean;
className?: string;
}
See the types documentation for all component contracts.
| Control | Schema Match | Component |
|---------|--------------|-----------|
| String | type: "string" | Input |format: "email"
| Email | | Input (type="email") |format: "password"
| Password | | Input (type="password") |format: "uri"
| URL | | Input (type="url") |format: "phone"
| Phone | or scope ends with phone | Input (type="tel") |type: "number"
| Number | or type: "integer" | Input (type="number") |type: "boolean"
| Boolean | | Checkbox |type: "boolean"
| Toggle | + options.format: "toggle" | Toggle |enum
| Select | or oneOf | Select |options.format: "radio"
| Radio | enum/boolean + | RadioGroup |options.format: "combobox"
| Combobox | enum + | Combobox |format: "date"
| Date | | DatePicker |format: "date-time"
| DateTime | | DateTimePicker |options.multi: true
| Textarea | string + | Textarea |options.format: "slider"
| Slider | number + | Slider |format: "currency"
| Currency | | Input (with formatting) |format: "file"
| File | or contentMediaType | FileUpload |
`tsx
import { createI18n } from '@apart-tech/jsonforms-kit';
import { useTranslations } from 'next-intl';
function useFormI18n(namespace: string) {
const t = useTranslations(namespace);
const tErrors = useTranslations('formErrors');
return createI18n({
translate: (key, defaultValue) => {
const translated = t(key);
return translated !== key ? translated : defaultValue;
},
translateError: (error) => {
const translated = tErrors(error.keyword);
return translated !== error.keyword ? translated : error.message;
},
locale: 'en',
});
}
// Usage
function MyForm() {
const i18n = useFormI18n('forms.personalInfo');
return (
i18n={i18n}
/>
);
}
`
Override default tester ranks or add custom testers:
`tsx
import { createRenderers, rankWith, scopeEndsWith } from '@apart-tech/jsonforms-kit';
const renderers = createRenderers({
components: componentMap,
testers: {
// Override default ranks
ranks: {
email: 10, // Higher priority for email fields
select: 4,
},
// Add custom testers
custom: [
{
name: 'specialPhone',
tester: rankWith(10, scopeEndsWith('mobileNumber')),
renderer: 'Input',
props: { type: 'tel' },
},
],
},
});
`
Creates JSON Forms renderer registry entries.
Creates a JSON Forms i18n configuration object.
React context provider for component map and class names.
- useComponents() - Access the component mapuseClassNames()
- - Access class name configurationuseFieldWrapper(props)
- - Common field wrapper logicuseHasComponent(name)` - Check if a component is available
-
MIT