@axenstudio/axen-form
v0.1.17
Published
Lightweight, config-driven, enterprise-grade React form library. Zero UI dependency. CSS Modules. A11y-aware.
Downloads
1,518
Maintainers
Readme
@axenstudio/axen-form
Lightweight, config-driven, enterprise-grade React form library.
Zero UI dependency. CSS Modules. A11y-aware. TypeScript strict.
Table of Contents
- @axenstudio/axen-form
Why axen-form?
| Feature | axen-form | Formik + MUI | React Hook Form | | ------------- | ------------------------------------- | --------------------- | ---------------- | | UI Dependency | None — native HTML | MUI (~350KB) | None | | Form Engine | Built-in reactive store | Formik rerender-heavy | Ref-based | | Validation | Plugin-based (Yup / Zod / custom) | Yup only | Resolver pattern | | Styling | CSS Modules + design tokens | Emotion runtime | BYO | | Config-Driven | Yes — one config = one form | No | No | | Bundle Size | < 15KB gzipped | ~350KB+ with MUI | ~10KB | | React Version | ≥ 19 | ≥ 16 | ≥ 16 | | TypeScript | Strict end-to-end | Partial | Good |
Installation
npm install @axenstudio/axen-form
# or
pnpm add @axenstudio/axen-formPeer dependencies: react ≥ 19, react-dom ≥ 19
Optional validation libraries:
# Pick one (or both)
pnpm add yup # Yup adapter
pnpm add zod # Zod adapterQuick Start
import { AxenForm, defaultComponentMap } from '@axenstudio/axen-form';
const config = {
initialValues: { name: '', email: '', role: 'user' },
fields: [
{ name: 'name', type: 'text', label: 'Full Name', required: true, colSpan: 6 },
{ name: 'email', type: 'email', label: 'Email', required: true, colSpan: 6 },
{
name: 'role',
type: 'select',
label: 'Role',
options: [
{ value: 'user', label: 'User' },
{ value: 'admin', label: 'Admin' },
],
},
],
};
function App() {
return (
<AxenForm
config={config}
components={defaultComponentMap}
onSubmit={(values) => console.log(values)}
/>
);
}That's it. One config, one component — a full form with theme, icons, placeholders, grid layout, validation, and accessibility out of the box.
10 Scenarios — Basic → Expert
axen-form is designed to scale from a quick contact form to a complex enterprise workflow. The following 10 scenarios illustrate the full capability range, from basic to expert-level usage.
BASIC
01 · Basic Form
The simplest form: a few fields, default layout, built-in validation.
import { AxenForm, defaultComponentMap } from '@axenstudio/axen-form';
const config = {
initialValues: { name: '', email: '' },
fields: [
{ name: 'name', type: 'text', label: 'Name', required: true },
{ name: 'email', type: 'email', label: 'Email', required: true },
],
};
<AxenForm config={config} components={defaultComponentMap} onSubmit={console.log} />;- Fields are full-width by default (colSpan = 12)
- Built-in
simpleValidatorvalidatesrequiredfields - Enterprise styling applied automatically (icons, tinted inputs, themed labels)
02 · All 19 Field Types
Render every built-in field type in a single form to explore the full component palette.
const config = {
initialValues: {
name: '',
email: '',
password: '',
phone: '',
bio: '',
age: 0,
price: 0,
rating: 50,
birthday: '',
alarm: '',
meeting: '',
country: '',
agree: false,
priority: 'medium',
darkMode: false,
city: '',
tags: [],
suggestion: '',
color: '#1976d2',
},
fields: [
// Text inputs
{ name: 'name', type: 'text', label: 'Full Name', colSpan: 6 },
{ name: 'email', type: 'email', label: 'Email', colSpan: 6 },
{ name: 'password', type: 'password', label: 'Password', colSpan: 6 },
{ name: 'phone', type: 'phone', label: 'Phone', colSpan: 6 },
{ name: 'bio', type: 'textarea', label: 'Biography', rows: 3 },
// Numeric
{ name: 'age', type: 'number', label: 'Age', min: 0, max: 150, colSpan: 4 },
{
name: 'price',
type: 'currency',
label: 'Price',
currency: 'USD',
locale: 'en-US',
colSpan: 4,
},
{ name: 'rating', type: 'slider', label: 'Rating', min: 0, max: 100, step: 5, colSpan: 4 },
// Date & Time
{ name: 'birthday', type: 'date', label: 'Birthday', colSpan: 4 },
{ name: 'alarm', type: 'time', label: 'Alarm', ampm: true, colSpan: 4 },
{ name: 'meeting', type: 'datetime', label: 'Meeting', colSpan: 4 },
// Selection
{
name: 'country',
type: 'select',
label: 'Country',
options: [
{ value: 'us', label: 'United States' },
{ value: 'id', label: 'Indonesia' },
],
colSpan: 6,
},
{
name: 'priority',
type: 'radio',
label: 'Priority',
options: [
{ value: 'low', label: 'Low' },
{ value: 'medium', label: 'Medium' },
{ value: 'high', label: 'High' },
],
colSpan: 6,
},
{ name: 'agree', type: 'checkbox', label: 'I agree to terms', colSpan: 6 },
{ name: 'darkMode', type: 'switch', label: 'Dark Mode', colSpan: 6 },
// Autocomplete
{ name: 'city', type: 'autocomplete', label: 'City', fetchOptions: searchCities, colSpan: 4 },
{ name: 'tags', type: 'autocomplete-multi', label: 'Tags', options: tagOptions, colSpan: 4 },
{
name: 'suggestion',
type: 'autocomplete-predict',
label: 'Suggestion',
fetchOptions: fetchSuggestions,
colSpan: 4,
},
// Miscellaneous
{ name: 'color', type: 'color', label: 'Theme Color', colSpan: 4 },
],
};INTERMEDIATE
03 · Layout System
Control column spans per breakpoint for responsive form layouts.
const config = {
initialValues: { firstName: '', lastName: '', email: '', address: '', city: '', zip: '' },
fields: [
// Side-by-side on tablet+, stacked on mobile
{ name: 'firstName', type: 'text', label: 'First Name', colSpan: { xs: 12, sm: 6 } },
{ name: 'lastName', type: 'text', label: 'Last Name', colSpan: { xs: 12, sm: 6 } },
// Full width
{ name: 'email', type: 'email', label: 'Email' },
{ name: 'address', type: 'text', label: 'Address' },
// Three across on desktop
{ name: 'city', type: 'text', label: 'City', colSpan: { xs: 12, sm: 6, md: 4 } },
{
name: 'state',
type: 'select',
label: 'State',
colSpan: { xs: 12, sm: 6, md: 4 },
options: stateOptions,
},
{ name: 'zip', type: 'text', label: 'ZIP', colSpan: { xs: 12, md: 4 } },
],
};
// colSpan breakpoints: xs (<600px), sm (≥600px), md (≥900px), lg (≥1200px), xl (≥1536px)See Layout System for Grid, Box, Stack, Divider, and Spacer components.
04 · Yup / Zod Validation
Bring your own validation library via adapters.
Yup Adapter:
import { yupAdapter } from '@axenstudio/axen-form/adapters/yup';
import * as yup from 'yup';
// Fields with required: true auto-generate schema.
// For custom rules, use the validation prop:
const config = {
initialValues: { email: '', age: 0 },
fields: [
{ name: 'email', type: 'email', label: 'Email', required: true },
{ name: 'age', type: 'number', label: 'Age', required: true, min: 18, max: 120 },
],
};
<AxenForm
config={config}
components={defaultComponentMap}
validationAdapter={yupAdapter}
onSubmit={handleSubmit}
/>;Zod Adapter:
import { zodAdapter } from '@axenstudio/axen-form/adapters/zod';
<AxenForm
config={config}
components={defaultComponentMap}
validationAdapter={zodAdapter}
onSubmit={handleSubmit}
/>;Built-in (zero-dep):
Without any adapter, axen-form uses simpleValidator — validates required fields automatically.
05 · Conditional / Hidden Fields
Hide fields dynamically based on form values. Hidden fields are excluded from validation and rendering.
const config = {
initialValues: { accountType: 'personal', companyName: '', taxId: '' },
fields: [
{
name: 'accountType',
type: 'select',
label: 'Account Type',
options: [
{ value: 'personal', label: 'Personal' },
{ value: 'business', label: 'Business' },
],
},
{
name: 'companyName',
type: 'text',
label: 'Company Name',
required: true,
hidden: (values) => values.accountType !== 'business',
},
{
name: 'taxId',
type: 'text',
label: 'Tax ID',
hidden: (values) => values.accountType !== 'business',
},
],
};ADVANCED
06 · Array Fields
Repeating field groups with add/remove controls.
import type { ArrayFieldConfig } from '@axenstudio/axen-form';
const config = {
initialValues: { contacts: [{ name: '', email: '' }] },
fields: [
{
name: 'contacts',
type: 'text',
isArray: true,
label: 'Contacts',
addLabel: '+ Add Contact',
removeLabel: 'Remove',
minItems: 1,
maxItems: 5,
fields: [
{ name: 'name', type: 'text', label: 'Name', colSpan: 6 },
{ name: 'email', type: 'email', label: 'Email', colSpan: 6 },
],
} satisfies ArrayFieldConfig,
],
};Array helpers available via arrayRef:
push(value)— add item at endremove(index)— remove itemswap(a, b)— swap two itemsinsert(index, value)— insert at positionreplace(index, value)— replace itemmove(from, to)— move item
07 · Ref Control
Access the form imperatively from parent components.
import { useRef } from 'react';
import type { AxenFormRef } from '@axenstudio/axen-form';
function MyPage() {
const formRef = useRef<AxenFormRef>(null);
return (
<>
<AxenForm
ref={formRef}
config={config}
components={defaultComponentMap}
onSubmit={handleSubmit}
/>
<div>
<button onClick={() => formRef.current?.submit()}>Submit from Outside</button>
<button onClick={() => formRef.current?.resetForm()}>Reset</button>
<button onClick={() => console.log(formRef.current?.getValues())}>Inspect Values</button>
<button onClick={() => console.log(formRef.current?.isValid())}>Check Valid</button>
</div>
</>
);
}| Method | Return | Description |
| -------------------- | ------------------------- | --------------------------------------- |
| submit() | void | Trigger form submission |
| resetForm(values?) | void | Reset form (optionally with new values) |
| getValues() | Record<string, unknown> | Get current form values |
| getErrors() | Record<string, string> | Get current validation errors |
| isValid() | boolean | Check if form has no errors |
| isDirty() | boolean | Check if values differ from initial |
08 · Payload Mapping
Control which fields are sent to onSubmit — prevent hidden/irrelevant fields from leaking to your API.
// Static whitelist — only name and email are submitted
<AxenForm
config={config}
components={defaultComponentMap}
payloadFields={['name', 'email']}
onSubmit={(values) => {
// values: { name, email } — nothing else
}}
/>
// Dynamic whitelist — different fields per accountType
<AxenForm
config={config}
components={defaultComponentMap}
payloadFields={{
personal: ['firstName', 'lastName', 'ssn'],
business: ['companyName', 'taxId', 'industry'],
}}
payloadDiscriminator="accountType"
onSubmit={handleSubmit}
/>EXPERT
09 · Form Context
Access form state from deeply nested child components using context hooks.
import { useFormContext, useField } from '@axenstudio/axen-form';
function StatusBadge() {
const { store } = useFormContext();
const state = store.getState();
return (
<span>
{state.dirty ? '● Unsaved changes' : '✓ Saved'}
{' | '}
Errors: {Object.keys(state.errors).length}
</span>
);
}
function InlineFieldDisplay({ name }: { name: string }) {
const { value, error, touched } = useField(name);
return (
<div>
<strong>{name}:</strong> {String(value)}
{touched && error && <span style={{ color: 'red' }}> — {error}</span>}
</div>
);
}
// Nest these inside AxenForm's children
<AxenForm config={config} components={defaultComponentMap} onSubmit={handleSubmit}>
<StatusBadge />
<InlineFieldDisplay name="email" />
</AxenForm>;Headless usage with useAxenForm:
import { useAxenForm } from '@axenstudio/axen-form';
function FullyCustomForm() {
const { store, formApi } = useAxenForm({
config: {
initialValues: { name: '' },
fields: [{ name: 'name', type: 'text', required: true }],
},
onSubmit: async (values) => {
/* ... */
},
});
return (
<form onSubmit={formApi.handleSubmit}>
<input
name="name"
value={formApi.values.name as string}
onChange={formApi.handleChange}
onBlur={formApi.handleBlur}
/>
{formApi.errors.name && <span>{formApi.errors.name}</span>}
<button type="submit">Submit</button>
</form>
);
}10 · Custom Component
Override any built-in field with your own component.
import type { FieldComponentProps } from '@axenstudio/axen-form';
// Custom star-rating component
function StarRating({ name, value, onChange, label, error, helperText }: FieldComponentProps) {
const stars = [1, 2, 3, 4, 5];
const current = Number(value) || 0;
return (
<div>
<label>{label}</label>
<div>
{stars.map((star) => (
<button
key={star}
type="button"
onClick={() => {
const syntheticEvent = { target: { name, value: star } };
onChange(syntheticEvent as any);
}}
style={{
color: star <= current ? 'gold' : '#ccc',
fontSize: '1.5rem',
background: 'none',
border: 'none',
}}
>
★
</button>
))}
</div>
{error && <span style={{ color: 'red', fontSize: '0.75rem' }}>{helperText}</span>}
</div>
);
}
// Per-field override
const config = {
initialValues: { rating: 0 },
fields: [{ name: 'rating', type: 'text', label: 'Your Rating', component: StarRating }],
};
// Or type-level override via components prop
<AxenForm
config={config}
components={{ ...defaultComponentMap, 'star-rating': StarRating }}
onSubmit={handleSubmit}
/>;Built-in Field Types (19)
Every field renders enterprise-grade out of the box — with icons, placeholders, themed styling, and WCAG-compliant focus rings.
Text Inputs
| Type | Component | Icon | Default Placeholder | Description |
| ---------- | --------------- | ----------- | ---------------------- | ---------------------------------- |
| text | TextField | ✏️ Pencil | Enter text... | Standard text input |
| email | EmailField | ✉️ Envelope | [email protected] | Email input with type="email" |
| password | PasswordField | 🔒 Lock | Enter password... | Password with show/hide toggle |
| phone | PhoneField | 📞 Phone | +1 (555) 000-0000 | Phone input with type="tel" |
| textarea | TextareaField | — | Enter description... | Multi-line text, configurable rows |
Numeric Inputs
| Type | Component | Icon | Default Placeholder | Description |
| ---------- | --------------- | ---------- | ------------------- | ----------------------------------------------------- |
| number | NumberField | # Hash | 0 | Locale-aware thousand separator + cursor preservation |
| currency | CurrencyField | $ Dollar | 0.00 | Intl currency formatting with prefix/suffix |
| slider | SliderField | — | — | Range slider with value display |
NumberField preserves cursor position after reformatting. Set locale (e.g., 'id-ID' for dot-thousands, comma-decimal) and min/max/step for constraints.
CurrencyField uses Intl.NumberFormat with currency (e.g., 'USD', 'IDR') and locale for automatic symbol and formatting.
Date & Time
| Type | Component | Icon | Default Placeholder | Description |
| ---------- | --------------- | ----------- | ---------------------- | ------------------------------------------------------------- |
| date | DateField | 📅 Calendar | Select date | Custom date picker — displays locale, stores ISO YYYY-MM-DD |
| time | TimeField | 🕐 Clock | Select time | Custom time picker — 12h/24h, stores ISO HH:mm |
| datetime | DateTimeField | 📅 Calendar | Select date and time | Combined date+time, stores ISO YYYY-MM-DDTHH:mm |
All date/time fields store ISO strings internally and display locale-formatted values. Set ampm: true for 12-hour mode, showSeconds: true to include seconds.
Selection
| Type | Component | Description |
| ---------- | --------------- | -------------------------- |
| select | SelectField | Native <select> dropdown |
| checkbox | CheckboxField | Styled checkbox with label |
| radio | RadioField | Radio group from options |
| switch | SwitchField | Toggle switch (on/off) |
Autocomplete
| Type | Component | Icon | Default Placeholder | Description |
| ---------------------- | -------------------------- | --------- | ------------------- | ---------------------------------- |
| autocomplete | AutocompleteField | 🔍 Search | Search... | Server-side search with debounce |
| autocomplete-multi | AutocompleteMultiField | — | Search... | Multi-select with chip/tag display |
| autocomplete-predict | AutocompletePredictField | 🔍 Search | Search... | Ghost text prediction + Tab accept |
AutocompleteField accepts fetchOptions: (query, signal?) => Promise<Option[]> for server-side search with built-in debounce and keyboard navigation (↑↓ Enter Esc).
AutocompletePredictField shows ghost text predictions that users accept with Tab — similar to AI autocomplete in code editors.
Miscellaneous
| Type | Component | Description |
| ------- | ------------ | ------------------------ |
| color | ColorField | Color swatch + hex input |
Enterprise Features
These features are active by default without extra configuration.
Built-in Icon System
Every field that accepts text input has a built-in SVG icon rendered inside the input (left side). Icons are inline SVGs (~200 bytes each), zero-dependency, tree-shakeable.
// Default icon (auto per field type)
{ name: 'email', type: 'email', label: 'Email' }
// Custom icon override
{ name: 'search', type: 'text', label: 'Search', icon: <MySearchIcon /> }
// Disable icon
{ name: 'plain', type: 'text', label: 'Plain', icon: false }Icon map:
| Field Type | Icon |
| ---------------------- | ---------------- |
| text | Pencil / Edit |
| email | Envelope |
| password | Lock |
| phone | Phone |
| number | Hash # |
| currency | Dollar $ |
| date, datetime | Calendar |
| time | Clock |
| autocomplete | Magnifying glass |
| autocomplete-predict | Magnifying glass |
Fields without icons: textarea, slider, select, checkbox, radio, switch, autocomplete-multi, color.
Default Placeholders
Fields without an explicit placeholder receive a sensible default based on their type (Enter text..., [email protected], Select date, etc.). Override with a custom string or set placeholder: '' to clear.
Field Groups / Sections
Group related fields under a visual section header. Groups are purely visual — they don't create nested data structures.
import type { FieldGroupConfig } from '@axenstudio/axen-form';
const config = {
initialValues: { firstName: '', lastName: '', email: '', phone: '', street: '', city: '' },
fields: [
{
group: 'Personal Information',
fields: [
{ name: 'firstName', type: 'text', label: 'First Name', colSpan: 6 },
{ name: 'lastName', type: 'text', label: 'Last Name', colSpan: 6 },
{ name: 'email', type: 'email', label: 'Email' },
{ name: 'phone', type: 'phone', label: 'Phone' },
],
} satisfies FieldGroupConfig,
{
group: 'Address',
fields: [
{ name: 'street', type: 'text', label: 'Street' },
{ name: 'city', type: 'text', label: 'City', colSpan: 6 },
],
} satisfies FieldGroupConfig,
],
};Group headers render as bold, themed labels with a subtle bottom border, spanning the full grid width.
Theme System
axen-form ships with 3 built-in themes applied via CSS custom properties. The default theme is auto-applied when no theme prop is set.
// Default blue theme (auto-applied)
<AxenForm config={config} components={defaultComponentMap} onSubmit={handleSubmit} />
// Subtle gray theme
<AxenForm config={config} components={defaultComponentMap} theme="subtle" onSubmit={handleSubmit} />
// Green theme
<AxenForm config={config} components={defaultComponentMap} theme="green" onSubmit={handleSubmit} />Built-in presets:
| Theme | Primary | Input Background | Label Color |
| --------- | ----------------- | ---------------- | ----------- |
| default | #1976d2 (Blue) | #f0f7ff | #1565c0 |
| subtle | #546e7a (Gray) | #f5f5f5 | #37474f |
| green | #2e7d32 (Green) | #f1f8e9 | #1b5e20 |
Custom themes: Create your own by adding a CSS rule targeting [data-axen-theme='yourTheme']:
[data-axen-theme='coral'] {
--axen-color-primary: #e74c3c;
--axen-color-primary-hover: #c0392b;
--axen-color-primary-light: #fde8e8;
--axen-color-input-bg: #fef5f5;
--axen-color-label: #c0392b;
}Then: <AxenForm theme="coral" ... />
Layout System
Grid Layout (12-column)
AxenForm renders fields inside a 12-column CSS Grid. Each field occupies columns based on its colSpan property. Default is full-width (colSpan: 12).
┌────────────────────────────────────────────────┐
│ colSpan: 12 (full width) │
├────────────────────────┬───────────────────────┤
│ colSpan: 6 (half) │ colSpan: 6 (half) │
├────────────┬───────────┼───────────────────────┤
│ colSpan: 4 │ colSpan: 4│ colSpan: 4 │
└────────────┴───────────┴───────────────────────┘The grid maxes out at 900px width and auto-centers — no external container needed.
Responsive colSpan
Use an object to set different spans per breakpoint. Values cascade upward (xs → sm → md → lg → xl).
{
name: 'city',
type: 'text',
label: 'City',
colSpan: {
xs: 12, // Full width on mobile (<600px)
sm: 6, // Half width on tablet (≥600px)
md: 4, // Third on desktop (≥900px)
},
}Breakpoints:
| Key | Min Width | Typical Device |
| ---- | --------- | ---------------- |
| xs | 0 | Mobile |
| sm | 600px | Tablet portrait |
| md | 900px | Tablet landscape |
| lg | 1200px | Desktop |
| xl | 1536px | Large desktop |
Or use a number for all breakpoints: colSpan: 6 = half-width on all screens.
Layout Components
In addition to config-driven form layout, axen-form exports standalone layout components with a MUI-like API, powered by CSS Grid/Flexbox.
Grid
12-column responsive grid — independent layout component.
import { Grid } from '@axenstudio/axen-form';
<Grid container spacing={2}>
<Grid size={{ xs: 12, md: 6 }}>Left half</Grid>
<Grid size={{ xs: 12, md: 6 }}>Right half</Grid>
</Grid>;Props: container, columns (default 12), spacing, rowSpacing, columnSpacing, size, xs/sm/md/lg/xl, offset.
Box
Generic flex/block container with spacing shortcuts.
import { Box } from '@axenstudio/axen-form';
<Box display="flex" gap="16px" p={2} justifyContent="space-between">
<span>Left</span>
<span>Right</span>
</Box>;Props: display, flexDirection, justifyContent, alignItems, flexWrap, gap, p/px/py, m/mx/my.
Stack
Vertical/horizontal stack with consistent gap.
import { Stack } from '@axenstudio/axen-form';
<Stack direction="column" spacing={2}>
<div>Item 1</div>
<div>Item 2</div>
</Stack>;Props: direction (row/column), spacing, alignItems, justifyContent, divider.
Divider
Horizontal or vertical separator line.
import { Divider } from '@axenstudio/axen-form';
<Stack direction="column" spacing={2}>
<div>Above</div>
<Divider />
<div>Below</div>
</Stack>;Spacer
Invisible spacing element. Uses 8px unit multiplier.
import { Spacer } from '@axenstudio/axen-form';
// Horizontal 16px gap
<Spacer x={2} />
// Push items apart (flex grow)
<Stack direction="row"><Logo /><Spacer grow /><NavLinks /></Stack>| Prop | Type | Description |
| ------- | --------- | ------------------------------------ |
| x | number | Horizontal spacing (width = x × 8px) |
| y | number | Vertical spacing (height = y × 8px) |
| basis | number | Flex basis (basis × 8px) |
| grow | boolean | Grow to fill remaining space |
Core Concepts
Config-Driven Forms
Define your entire form with a single configuration object — fields, layout, validation, and behavior:
import type { FormConfig } from '@axenstudio/axen-form';
const config: FormConfig = {
initialValues: { name: '', email: '', age: 0 },
fields: [
{ name: 'name', type: 'text', label: 'Name', required: true },
{
name: 'email',
type: 'email',
label: 'Email',
required: true,
requiredMessage: 'Email wajib diisi',
},
{ name: 'age', type: 'number', label: 'Age', min: 0, max: 150, locale: 'en-US' },
],
};Reactive Form State
axen-form uses a built-in reactive store (no Formik dependency). State is managed via useSyncExternalStore — only the fields that change re-render.
- Field-level subscription: components subscribe to specific field paths
- Dirty detection: deep comparison against initial values
- Sync validation: runs on change/blur (configurable)
Validation Adapters
Plug in any validation library:
| Adapter | Import Path | Library |
| ----------------- | ------------------------------------ | ------- |
| yupAdapter | @axenstudio/axen-form/adapters/yup | Yup |
| zodAdapter | @axenstudio/axen-form/adapters/zod | Zod |
| simpleValidator | Built-in (default) | None |
Both adapters auto-generate schemas from FieldConfig (required, min, max, type-aware schema). You can also provide custom validation on individual fields.
Advanced Features
Payload Whitelist
Control which fields are sent to onSubmit. See Scenario 08.
Headless Usage
Use useAxenForm for full rendering control. See Scenario 09.
Custom Field Components
Override any built-in field. See Scenario 10.
API Reference
<AxenForm>
Main config-driven form component.
| Prop | Type | Default | Description |
| ---------------------- | -------------------------------------- | ----------------- | ------------------------------------------- |
| config | FormConfig | required | Form configuration (fields + initialValues) |
| onSubmit | (values, meta) => void | required | Submit handler |
| components | FieldTypeComponentMap | {} | Component map (use defaultComponentMap) |
| validationAdapter | ValidationAdapter | simpleValidator | Validation library adapter |
| ref | Ref<AxenFormRef> | — | Imperative handle |
| theme | 'default' \| 'subtle' \| string | 'default' | Theme preset or custom name |
| columns | number | 12 | Grid columns |
| gap | string \| number | '1rem' | Grid gap |
| disabled | boolean | false | Disable all fields |
| className | string | — | Additional CSS class |
| payloadFields | string[] \| Record<string, string[]> | — | Payload whitelist |
| payloadDiscriminator | string | — | Discriminator field for dynamic payload |
| validateOnChange | boolean | true | Validate on field change |
| validateOnBlur | boolean | true | Validate on field blur |
| children | ReactNode | — | Extra content inside form (buttons, etc.) |
FieldConfig
Configuration for a single field.
| Property | Type | Description |
| ------------------ | --------------------------------------- | -------------------------------------------------- |
| name | string | Field name (supports nested paths: address.city) |
| type | BuiltInFieldType \| string | Field type identifier |
| label | string | Field label |
| placeholder | string | Input placeholder (auto-filled if not set) |
| required | boolean | Mark as required |
| requiredMessage | string | Custom required error message |
| disabled | boolean | Disable this field |
| readOnly | boolean | Read-only mode |
| hidden | boolean \| (values) => boolean | Conditional hiding |
| component | ComponentType | Override component for this field |
| options | FieldOption[] | Options for select/radio/autocomplete |
| icon | ReactNode \| false | Custom icon, or false to disable |
| colSpan | number \| { xs?, sm?, md?, lg?, xl? } | Grid column span (default: 12) |
| helperText | string | Helper text below field |
| min, max | number | Numeric/date constraints |
| step | number | Numeric step value |
| prefix, suffix | string | Display prefix/suffix (currency) |
| locale | string | Locale for number/date formatting |
| currency | string | Currency code (e.g., 'USD', 'IDR') |
| rows | number | Textarea rows |
| ampm | boolean | 12-hour mode for time/datetime |
| showSeconds | boolean | Show seconds in time picker |
| fetchOptions | (query, signal?) => Promise<Option[]> | Async option fetcher (autocomplete) |
| optionLabel | string | Template: '{name} - {email}' |
| multiple | boolean | Multi-select for autocomplete |
| fieldProps | Record<string, unknown> | Extra props forwarded to component |
| className | string | Additional CSS class for field wrapper |
FieldGroupConfig
| Property | Type | Description |
| ----------- | --------------------------------------- | --------------------------- |
| group | string | Section header label |
| fields | FieldConfig[] | Fields within the group |
| columns | number | Sub-grid columns (optional) |
| colSpan | number \| { xs?, sm?, md?, lg?, xl? } | Group wrapper column span |
| className | string | Additional CSS class |
Hooks
| Hook | Signature | Description |
| ---------------- | ------------------------------------------------------------------------- | ------------------------------- |
| useAxenForm | (opts) => { store, formApi } | Create a headless form instance |
| useField | (name) => { value, error, touched, onChange, onBlur } | Subscribe to a single field |
| useFieldArray | (name) => { fields, push, remove, swap, insert, replace, move, length } | Array field operations |
| useFormContext | () => { store, fields, components, disabled } | Access form context |
Utilities
| Utility | Description |
| ----------------------------------- | -------------------------------------- |
| mapPayload(values, fields, disc) | Apply payload whitelist to form values |
| pickFields(values, names) | Pick specific fields from values |
| getStrippedFields(values, names) | Get fields that would be stripped |
| get(obj, path, default?) | Deep get via dot path |
| set(obj, path, value) | Immutable deep set |
| formatNumber(value, opts) | Format number with locale |
| formatDateLocale(isoDate, locale) | Format ISO date for display |
| formatTimeLocale(isoTime, ...) | Format ISO time for display |
| isFieldGroup(item) | Type guard for FieldGroupConfig |
Architecture
src/
├── core/ # Framework-agnostic form engine
│ ├── formStore.ts # Reactive state (useSyncExternalStore)
│ ├── validation.ts # Adapter interface + simpleValidator
│ ├── pathUtils.ts # Nested path get/set/toArray
│ ├── numberUtils.ts # Locale-aware number formatting
│ ├── dateUtils.ts # ISO ↔ locale date conversion
│ └── payloadUtils.ts # Payload whitelist filter
│
├── react/ # React 19+ bindings
│ ├── AxenForm.tsx # Main config-driven renderer
│ ├── AxenField.tsx # Standalone field wrapper
│ ├── AxenArrayField.tsx # Array field renderer
│ ├── useAxenForm.ts # Headless form hook
│ ├── useField.ts # Single field subscription
│ ├── useFieldArray.ts # Array field operations
│ └── FormContext.ts # React context
│
├── fields/ # 19 built-in field components
│ ├── icons.tsx # Inline SVG icons
│ ├── iconUtils.ts # Icon resolution logic
│ ├── defaultPlaceholders.ts # Default placeholder map
│ ├── defaultComponentMap.ts # Type → Component registry
│ ├── fieldUtils.ts # Shared utilities (cx, aria)
│ ├── fieldBase.module.css # Shared field styles
│ ├── TextField/
│ ├── EmailField/
│ ├── PasswordField/ # Show/hide toggle
│ ├── PhoneField/
│ ├── TextareaField/
│ ├── NumberField/ # Cursor-preserving formatting
│ ├── CurrencyField/ # Intl currency
│ ├── SliderField/
│ ├── DateField/ # Custom DatePicker
│ ├── TimeField/ # Custom TimePicker
│ ├── DateTimeField/
│ ├── SelectField/
│ ├── CheckboxField/
│ ├── RadioField/
│ ├── SwitchField/
│ ├── AutocompleteField/ # Server search + debounce
│ ├── AutocompleteMultiField/ # Multi-select chips
│ ├── AutocompletePredictField/ # Ghost text
│ └── ColorField/
│
├── layout/ # Grid, Box, Stack, Divider, Spacer
├── tokens/ # CSS tokens, themes, reset
├── adapters/ # yupAdapter, zodAdapter
├── types.ts # All TypeScript definitions
└── index.ts # Public API barrelTheming with CSS Custom Properties
axen-form uses CSS Custom Properties (design tokens) for fine-grained theming.
Key token categories:
| Category | Example Tokens |
| ---------- | --------------------------------------------------------------------- |
| Colors | --axen-color-primary, --axen-color-error, --axen-color-input-bg |
| Typography | --axen-font-family, --axen-font-size-sm/md/lg |
| Spacing | --axen-space-xs/sm/md/lg/xl |
| Borders | --axen-color-border, --axen-border-radius |
| Focus | --axen-shadow-focus, --axen-color-border-focus |
| Fields | --axen-field-height, --axen-field-min-height (44px touch target) |
Built-in A11y support:
@media (prefers-color-scheme: dark)— dark mode tokens@media (prefers-contrast: more)— high contrast overrides@media (prefers-reduced-motion: reduce)— reduced motionfocus-visiblerings for keyboard navigationaria-invalid,aria-describedbyon all fields- Minimum 44px touch targets (WCAG 2.1 AA)
Browser Support
| Browser | Version | | ------- | --------------- | | Chrome | Last 2 versions | | Firefox | Last 2 versions | | Safari | 15.4+ | | Edge | Last 2 versions |
Requires React 19+ and ES2022+ environment.
License
MIT © Axen Studio
