npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@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

Readme

@axenstudio/axen-form

Lightweight, config-driven, enterprise-grade React form library.
Zero UI dependency. CSS Modules. A11y-aware. TypeScript strict.

npm license react


Table of Contents


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-form

Peer dependencies: react ≥ 19, react-dom ≥ 19

Optional validation libraries:

# Pick one (or both)
pnpm add yup        # Yup adapter
pnpm add zod        # Zod adapter

Quick 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 simpleValidator validates required fields
  • 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 end
  • remove(index) — remove item
  • swap(a, b) — swap two items
  • insert(index, value) — insert at position
  • replace(index, value) — replace item
  • move(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 barrel

Theming 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 motion
  • focus-visible rings for keyboard navigation
  • aria-invalid, aria-describedby on 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