apx-dsv0.1Local renderer · live
K
Foundations4
  • Getting started
  • Theming
  • Templates
  • Icons
  • Avatar
  • Badge
  • DataGrid
  • Scheduler
  • Stat
  • Table
  • Timeline
  • TreeView
  • Accordion
  • Alert
  • EmptyState
  • Progress
  • Skeleton
  • Spinner
  • SplashScreen
  • Toast
  • ColorPicker
  • FileUpload
  • Form
  • Rating
  • TagsInput
  • Combobox
  • Field
  • Select
  • Toggle
  • Button
  • Calendar
  • Checkbox
  • DatePicker
  • Input
  • NumberInput
  • Radio
  • Slider
  • Switch
  • Textarea
  • AppShell
  • Div
  • Divider
  • Sidebar
  • Stack
  • Typography
  • Image
  • Breadcrumbs
  • Carousel
  • NavigationMenu
  • Pagination
  • Stepper
  • Tabs
  • Toolbar
  • CommandPalette
  • Confirm
  • Drawer
  • HoverCard
  • Menu
  • Modal
  • Popover
  • Tooltip
  • Icon
  • Card
  • PricingCard
60 componentsapx-ds/renderer
Form
Variant↳ other

Form

Form

Form state engine — useForm hook + <Form> + <FormField> trio for managing values, errors, touched state, and submission. Pairs with <Field> for zero-prop-wiring forms.

Form

A dependency-free form state engine + JSX wrappers. The point: turn the "I have 8 inputs and want errors / submission / dirty tracking" problem into one <Form initialValues onSubmit> without reaching for React Hook Form / Formik / your own useState soup.

Overview — validation, errors, and submit in one form

Loading preview…
Overview.tsx

Three pieces, useful independently:

ExportWhat it does
useForm()Headless engine. State + helpers + handleChange / handleBlur / handleSubmit.
<Form><form noValidate> that owns a useForm and publishes the live FormApi on context.
<FormField>Name-bound adapter. Reads form state for name, wires the inner control automatically.

You can use one, two, or all three — they compose cleanly with <Field>.

Why this (instead of RHF / Formik)?

Formik is unmaintained. React Hook Form is excellent but its API surface can be overkill for "I have a settings page." This trio stays lightweight and pairs with the controls you already have wired through <Field> for a11y.

It's not schema-first — no Yup / Zod / Joi adapter baked in. The validate prop is a plain (values) => errors function; Zod is a 5-line safeParse wrapper at the call site (see the example).

Quick start

tsx
import { Button, Form, FormField, Input } from 'apx-ds';

<Form
  initialValues={{ email: '', password: '' }}
  validate={(v) => {
    const errors: Record<string, string> = {};
    if (!v.email) errors.email = 'Email is required';
    if (v.password.length < 8) errors.password = 'Min 8 characters';
    return errors;
  }}
  onSubmit={async (values) => {
    await api.signIn(values);
  }}
>
  <FormField name="email" label="Email" required>
    <Input type="email" />
  </FormField>
  <FormField name="password" label="Password" required>
    <Input type="password" />
  </FormField>
  <Button type="submit" variant="solid">Sign in</Button>
</Form>
import { Button, Form, FormField, Input } from 'apx-ds';

<Form
  initialValues={{ email: '', password: '' }}
  validate={(v) => {
    const errors: Record<string, string> = {};
    if (!v.email) errors.email = 'Email is required';
    if (v.password.length < 8) errors.password = 'Min 8 characters';
    return errors;
  }}
  onSubmit={async (values) => {
    await api.signIn(values);
  }}
>
  <FormField name="email" label="Email" required>
    <Input type="email" />
  </FormField>
  <FormField name="password" label="Password" required>
    <Input type="password" />
  </FormField>
  <Button type="submit" variant="solid">Sign in</Button>
</Form>

That's the whole API. No register, no Controller, no withFormik.

Patterns

Basic — controlled values + submit

Loading preview…
Basic.tsx

Sign-up — email / password / terms-checkbox with central validate()

Loading preview…
SignUp.tsx

Settings page — Input + Textarea + reset button

Loading preview…
SettingsPage.tsx

Async username check — debounced + abortable

Loading preview…
AsyncValidation.tsx

Per-field validator instead of central validate

Loading preview…
PerFieldValidator.tsx

Server-rejected fields via helpers.setErrors()

Loading preview…
ServerErrors.tsx

“You have unsaved changes” via form.isDirty

Loading preview…
DirtyWarning.tsx

Reset to initial vs reset to a fresh values object

Loading preview…
ResetForm.tsx

Zod / Yup integration in 8 lines — no bundled adapter

Loading preview…
ZodIntegration.tsx

enableReinitialize — switch between two profiles

Loading preview…
EnableReinitialize.tsx

Render-prop access to the live FormApi

Loading preview…
RenderProp.tsx

useForm without <Form> — pair with a native <form>

Loading preview…
HeadlessHook.tsx

Every DS form control auto-bound via <FormField binding="...">

Loading preview…
EveryControl.tsx

API

useForm<Values>(options)

OptionTypeDefaultNotes
initialValuesValues—Required.
validate(values) => errors | Promise<errors>—Central validator. Per-field validators override on collision.
validateOn'submit' | 'blur' | 'change' | 'submit-and-blur''submit-and-blur'UX default: don't yell mid-typing.
validateOnMountbooleanfalseRun validate once at mount (useful for "save-as-you-edit" UIs).
enableReinitializebooleanfalseRe-init when initialValues identity changes. Footgun: opt-in.
onSubmit(values, helpers) => void | Promise<void>—Returning a promise sets isSubmitting.
onReset(values) => void—Fired after resetForm().
focusOnErrorbooleantrueAfter a failed submit, focuses the first invalid field's control.

Returns a FormApi<Values> — full state + helpers + native handlers.

<Form initialValues onSubmit>

Renders <form noValidate onSubmit={form.handleSubmit}> and publishes the live FormApi on FormContext. Accepts every option useForm accepts plus all <form> HTML attributes. Children can be regular React nodes (recommended) or a (form) => ReactNode render-prop.

<FormField name binding label …>

The adapter. Reads form state for name, wires the inner control via cloneElement, registers per-field validators + the control id for focusOnError.

PropTypeDefaultNotes
namestring—Required. Must exist on initialValues.
binding'native' | 'checkbox' | 'value''native'How the child receives state — see below.
validate(value, values) => string | null—Sync per-field validator.
validateAsync(value, { signal }) => Promise<string | null>—Debounced + abortable async validator.
validateDebounceMsnumber300Async debounce window.
(all Field props)——label / description / helperText / required / size / …

Binding strategies

bindingUse withWires
'native'<Input>, <Textarea>, <NumberInput>, plain <input> / <select> / <textarea>value, onChange(event), onBlur(event)
'checkbox'<Checkbox>, <Switch>checked, onCheckedChange(boolean), onChange(event) (fallback)
'value'<Combobox>, <Select>, <Rating>, <TagsInput>, <Slider>, <RadioGroup>value, onChange(value), onBlur(event)

We don't auto-sniff displayName — it's brittle across forwardRef wrappers + minification. An explicit one-word binding prop is honest and predictable.

FormApi<Values> shape

ts
interface FormApi<Values> {
  // State
  values: Values;
  initialValues: Values;
  errors: Partial<Record<keyof Values, string>>;
  touched: Partial<Record<keyof Values, boolean>>;
  dirty: Partial<Record<keyof Values, boolean>>;
  isSubmitting: boolean;
  submitCount: number;
  isValid: boolean;
  isDirty: boolean;

  // Helpers
  setFieldValue(name, value): void;
  setFieldError(name, error): void;
  setFieldTouched(name, touched): void;
  setErrors(errors): void;
  setTouched(touched): void;
  setValues(partial): void;
  resetForm(next?: { values }): void;
  validateForm(): Promise<errors>;
  submitForm(): Promise<void>;

  // Native handlers
  handleChange(event): void;
  handleBlur(event): void;
  handleSubmit(event?): Promise<void>;

  // Registration (used by <FormField>)
  registerFieldValidator(name, validator): () => void;
  registerFieldId(name, id): () => void;
}
interface FormApi<Values> {
  // State
  values: Values;
  initialValues: Values;
  errors: Partial<Record<keyof Values, string>>;
  touched: Partial<Record<keyof Values, boolean>>;
  dirty: Partial<Record<keyof Values, boolean>>;
  isSubmitting: boolean;
  submitCount: number;
  isValid: boolean;
  isDirty: boolean;

  // Helpers
  setFieldValue(name, value): void;
  setFieldError(name, error): void;
  setFieldTouched(name, touched): void;
  setErrors(errors): void;
  setTouched(touched): void;
  setValues(partial): void;
  resetForm(next?: { values }): void;
  validateForm(): Promise<errors>;
  submitForm(): Promise<void>;

  // Native handlers
  handleChange(event): void;
  handleBlur(event): void;
  handleSubmit(event?): Promise<void>;

  // Registration (used by <FormField>)
  registerFieldValidator(name, validator): () => void;
  registerFieldId(name, id): () => void;
}

A11y

  • <Form noValidate> — we own validation, browser stays out of the way.
  • After a failed submit, all fields are marked touched=true so every error is announced.
  • focusOnError (on by default) jumps focus to the first invalid control by id.
  • A single off-screen aria-live="polite" region per <Form> announces error-count transitions.
  • <FormField> inherits all <Field> a11y wiring (label association, aria-invalid, aria-describedby, required indicator, error role="alert").
  • axe-core: 0 violations across signup / settings / async / error-server / disabled / dirty states.

When to use what

Want…Use
Just JSX, no fuss<Form> + <FormField>
Full control over the <form> element / event flowuseForm() + your own <form>
To pair with <Field> you've already wired manuallyuseForm() + spread form.getFieldProps (manual; see HeadlessHook example)
A Zod / Yup schemaWrap it inside validate — no adapter needed (see ZodIntegration example)

See also

  • <Field> — the wrapper <FormField> renders underneath.
  • useFormFieldA11y — the shared hook every form control already calls; ensures Field + Form integration is 100% additive with zero source-code changes to existing controls.
  • <TagsInput> / <Combobox> / <Select> / <Rating> — value-callback controls; bind via <FormField binding="value">.
  • <Checkbox> / <Switch> — boolean toggles; bind via <FormField binding="checkbox">.

Props

PropTypeDefaultDescription
children*ReactNode | ((form: FormApi<Record<string, unknown>>) => ReactNode)—React children OR a render-prop receiving the live `FormApi`.
initialValues*Record<string, unknown>—Initial form values.
onSubmit*(values: Record<string, unknown>, helpers: FormHelpers<Record<string, unknown>>) => void | Promise<void>—Submit handler — receives `(values, helpers)`. Returning a promise sets `isSubmitting`.
enableReinitializeboolean—Re-init the form when `initialValues` identity changes. Default `false` (footgun).
focusOnErrorboolean—After a failed submit, focus the first invalid control. Default `true`.
noValidateboolean—Disable the browser's native HTML5 form validation (we own it). Default `true`.
onReset((values: Record<string, unknown>) => void)—Optional reset hook.
refRef<HTMLFormElement>—Allows getting a ref to the component instance. Once the component unmounts, React will set `ref.current` to `null` (or call the ref with `null` if you passed a callback ref). @see {@link https://react.dev/learn/referencing-values-with-refs#refs-and-the-dom React Docs}
sxSx——
validateFormValidator<Record<string, unknown>>—Optional central validator. Field-level validators win on collision.
validateOnenum—When validators fire. Default `'submit-and-blur'`.
validateOnMountboolean—Run `validate` on mount. Default `false`.