Skip to content

manipratap2/hookra

Hookra

JSON-driven form builder on top of React Hook Form + Chakra UI.
Better DX than RJSF. Type-safe. Tree-shakable. Zero config.

npm install hookra

Hookra uses Chakra UI v3 for its UI. If you don't already have it set up, install the peer dependencies:

npm install @chakra-ui/react @emotion/react react-hook-form

Getting started

Step 1 — Wrap your app with ChakraProvider

Hookra renders Chakra UI components, so your app needs a ChakraProvider at the root. If you already have one, skip this step.

// main.tsx (or index.tsx)
import { StrictMode } from 'react'
import { createRoot } from 'react-dom/client'
import { ChakraProvider, defaultSystem } from '@chakra-ui/react'
import { App } from './App'

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <ChakraProvider value={defaultSystem}>
      <App />
    </ChakraProvider>
  </StrictMode>,
)

Custom theme? Pass your own system instead of defaultSystem:

import { createSystem, defaultConfig } from '@chakra-ui/react'
const system = createSystem(defaultConfig, { theme: { /* overrides */ } })
<ChakraProvider value={system}></ChakraProvider>

Step 2 — Define a schema

// schema.ts
import type { FormSchema } from 'hookra'

export const contactSchema: FormSchema = {
  title: 'Contact Us',
  fields: [
    { name: 'name',    type: 'text',     label: 'Name',    required: true },
    { name: 'email',   type: 'email',    label: 'Email',   required: true },
    { name: 'message', type: 'textarea', label: 'Message', rows: 4 },
  ],
}

Step 3 — Render <FormBuilder>

// ContactForm.tsx
import { FormBuilder } from 'hookra'
import { contactSchema } from './schema'

export function ContactForm() {
  return (
    <FormBuilder
      schema={contactSchema}
      onSubmit={(data) => console.log(data)}
    />
  )
}

That's it. The form renders with validation, labels, and a submit button out of the box.

Field types

type Description
text email password url tel search Text input variants
number integer Numeric input with stepper
textarea Multi-line text (optional char count)
boolean switch Toggle switch
checkbox Single checkbox
select Native dropdown
multiselect Multi-select via checkboxes
radio Radio button group
checkboxgroup Checkbox group (multi-value)
date time datetime Date/time pickers
file File upload (accept, maxSize)
color Color picker
slider Range slider
hidden Hidden field (value included in submit)
array Dynamic list — add/remove rows
object Nested group of fields
custom Your own component via registry

Schema reference

FormSchema

{
  title?: string
  description?: string
  layout?: { columns?: number }   // grid columns (default 1)
  fields?: FieldSchema[]          // flat list
  sections?: FormSection[]        // grouped with titles
  submitLabel?: string
  showReset?: boolean
  resetLabel?: string
}

Common field properties

{
  name: string          // required — used as form value key
  type: FieldType       // required
  label?: string
  description?: string  // helper text
  placeholder?: string
  defaultValue?: unknown
  required?: boolean | string   // true or custom error message
  disabled?: boolean
  readOnly?: boolean
  hidden?: boolean      // excludes from render AND form values
  width?: 'full' | 'half' | 'third' | 'quarter' | 'two-thirds' | 'three-quarters' | 1–12
  validation?: FieldValidation
  dependsOn?: Condition // conditional visibility
  props?: Record<string, any>   // forwarded to Chakra component
}

Validation

validation: {
  required?: boolean | string
  min?: number | { value: number; message: string }
  max?: number | { value: number; message: string }
  minLength?: number | { value: number; message: string }
  maxLength?: number | { value: number; message: string }
  pattern?: string | { value: string; message: string }  // regex
  validate?: Record<string, (value) => boolean | string>
}

Conditions (dependsOn)

Simple — show when country equals "us":

{ "field": "country", "value": "us" }

With operator:

{ "field": "age", "operator": "gte", "value": 18 }

Compound — AND:

{ "all": [
  { "field": "country", "value": "us" },
  { "field": "role", "operator": "ne", "value": "guest" }
]}

Compound — OR:

{ "any": [
  { "field": "plan", "value": "pro" },
  { "field": "plan", "value": "enterprise" }
]}

Negation:

{ "not": { "field": "subscribed", "operator": "truthy" } }

All operators

Operator Meaning
eq == (default)
ne !=
gt gte lt lte Numeric comparisons
in Value is in array
nin Value is NOT in array
contains String contains
startsWith endsWith String start/end
matches Regex test
empty notEmpty null/undefined/''/""
truthy falsy Boolean coercion

Array fields

{
  name: 'phoneNumbers',
  type: 'array',
  label: 'Phone Numbers',
  minItems: 1,
  maxItems: 5,
  addLabel: 'Add phone',
  itemSchema: {
    type: 'object',
    name: 'phone',
    fields: [
      { name: 'type',   type: 'select', label: 'Type', options: [...] },
      { name: 'number', type: 'tel',    label: 'Number' },
    ],
  },
}

Object (nested) fields

{
  name: 'address',
  type: 'object',
  label: 'Address',
  collapsible: true,
  fields: [
    { name: 'street', type: 'text', label: 'Street', width: 'full' },
    { name: 'city',   type: 'text', label: 'City' },
    { name: 'zip',    type: 'text', label: 'Zip' },
  ],
}

Sections

sections: [
  {
    title: 'Personal Info',
    description: 'Your basic details',
    columns: 2,
    collapsible: true,
    dependsOn: { field: 'type', value: 'individual' },
    fields: [...],
  },
]

Multi-column layout

Set layout.columns on the form, then override per-field with width:

{
  layout: { columns: 3 },
  fields: [
    { name: 'a', type: 'text', label: 'A' },          // 1 col
    { name: 'b', type: 'text', label: 'B', width: 'full' },   // all 3 cols
    { name: 'c', type: 'text', label: 'C', width: 'two-thirds' }, // 2 cols
  ],
}

<FormBuilder> props

<FormBuilder
  schema={schema}                 // required
  onSubmit={(data) => {}}         // required
  onCancel={() => {}}             // optional
  defaultValues={{ name: 'Joe' }} // override schema defaults
  registry={{ myField: MyComp }}  // custom field components
  readOnly={false}
  loading={false}
  mode="onBlur"                   // RHF validation mode
  submitButton={<Button>Save</Button>}  // custom submit
  cancelButton={null}             // hide cancel
/>

Accessing RHF methods (ref)

import { useRef } from 'react'
import { FormBuilder, type FormBuilderRef } from 'hookra'

const ref = useRef<FormBuilderRef>(null)

<FormBuilder ref={ref} schema={schema} onSubmit={handleSubmit} />

// Programmatic submit / reset
ref.current?.submit()
ref.current?.reset()
ref.current?.form.setValue('email', 'new@example.com')
ref.current?.form.watch('country')

Custom field components

import { FormBuilder, createRegistry, defaultRegistry } from 'hookra'

function StarRating({ field, name }) {
  const { control } = useFormContext()
  // ... your implementation
}

<FormBuilder
  schema={schema}
  registry={{ starRating: StarRating }}
  onSubmit={handleSubmit}
/>

In the schema:

{ name: 'rating', type: 'custom', component: 'starRating', label: 'Rating' }

Tree-shaking

The package is marked "sideEffects": false. If you only use a subset of field types the unused field modules are eliminated by your bundler automatically.

You can also use individual field components directly:

import { TextField, SelectField } from 'hookra'

Peer dependencies

react >= 18
react-dom >= 18
react-hook-form >= 7
@chakra-ui/react >= 3
@emotion/react >= 11

All of the above must be installed in your project. See Getting started for the install command.

Dev / Demo

npm install
npm run dev        # Vite dev server with full demo on http://localhost:5173
npm run build      # Build library → dist/
npm run typecheck  # TypeScript check with no emit

License

MIT

About

Build dynamic, type-safe React forms from JSON schemas. 17+ field types, conditional logic, nested structures, validation — zero boilerplate. Powered by React Hook Form + Chakra UI.

Resources

License

Code of conduct

Contributing

Security policy

Stars

Watchers

Forks

Packages

 
 
 

Contributors