PixelForm

Validated form on each framework's form library (React Hook Form, VeeValidate, Angular reactive forms): its form, field, item, label, control, description and message parts auto-wire ids and aria-* across each field.

  • Compound API for composable forms: form, field, item, label, control, description and message (PixelForm.Root + .Field + … in React, PixelForm + PixelFormField + … in Vue, form[pxlForm] + [pxlFormField] + … in Angular).
  • Auto-generates linked ids and wires aria-describedby + aria-invalid on the controlled field.
  • Built on each framework's form library — React Hook Form, VeeValidate, Angular reactive forms — so any control it binds works: value/onChange/ref in React, v-model in Vue, a ControlValueAccessor in Angular.
  • The message shows the field error, as an alert, while there is one; in React and Vue, content you give it shows instead.
  • Surface-aware: a surface on the form, label, description or message follows kit-wide design tokens.
Status
stable
Category
forms
Since
v1.8.0

API

import { PixelForm } from '@pxlkit/ui-kit';

PixelForm

PropTypeDefaultDescription
form (required)UseFormReturn<T>—The form: what React Hook Form's useForm returns.
onSubmit (required)(data: T) => void | Promise<void>—Called with the form's values, once a submission finds no error.
children (required)React.ReactNode—The fields.
classNamestring—Extra classes on the <form>.
surface'pixel' | 'linear'—Surface override; defaults to the nearest provider.

ref points to <form>.

PixelForm.Root is the same component.

PixelForm.Field

PropTypeDefaultDescription
name (required)TName—Path of the field in the form's values.
rulesOmit<RegisterOptions<TFieldValues, TName>, 'valueAsNumber' | 'valueAsDate' | 'setValueAs' | 'disabled'>—Validation rules: React Hook Form's rules (required, min, max, minLength, maxLength, pattern, validate).
defaultValueUseControllerProps<TFieldValues, TName>['defaultValue']—Initial value, where the form's defaultValues have none.
shouldUnregisterboolean—Drops the field's value from the form when it unmounts; by default the value stays.
render (required)(args: { field: ControllerRenderProps<TFieldValues, TName>; fieldState: ControllerFieldState }) => React.ReactElement—Renders the control: gets the field's props (field: its value, change and blur handlers, name and ref) and its state (fieldState).

PixelForm.Item

PropTypeDefaultDescription
children (required)React.ReactNode—PixelForm.Label, PixelForm.Control, PixelForm.Description and PixelForm.Message.

Also takes the native attributes and event handlers of <div> (HTMLAttributes<HTMLDivElement>).

ref points to <div>.

PixelForm.Label

PropTypeDefaultDescription
surface'pixel' | 'linear'—Surface override; defaults to the nearest provider.

Also takes the native attributes and event handlers of <label> (LabelHTMLAttributes<HTMLLabelElement>).

ref points to <label>.

PixelForm.Control

PropTypeDefaultDescription
children (required)React.ReactElement—The control: one element, which gets the field's id, aria-describedby and aria-invalid.

ref points to its element (HTMLElement).

PixelForm.Description

PropTypeDefaultDescription
surface'pixel' | 'linear'—Surface override; defaults to the nearest provider.

Also takes the native attributes and event handlers of <p> (HTMLAttributes<HTMLParagraphElement>).

ref points to <p>.

PixelForm.Message

PropTypeDefaultDescription
surface'pixel' | 'linear'—Surface override; defaults to the nearest provider.

Also takes the native attributes and event handlers of <p> (HTMLAttributes<HTMLParagraphElement>).

ref points to <p>.

Accessibility

WCAG target: 2.1 AA

  • form
  • labelled-control

Each item generates a stable id base and links the label (for), the control (id + aria-describedby + aria-invalid), the description (id) and the message (id, role="alert" on error) automatically — authors do not pass ids manually.

Keyboard

KeyActionWhen
TabMove focus between form controls.—
EnterSubmit the form from any focused input.—

Usage

import { useForm } from 'react-hook-form';
import { PixelForm, PixelInput } from '@pxlkit/ui-kit';

type DefaultValues = {
  username: string;
  email: string;
};

export function Default() {
  const form = useForm<DefaultValues>({
    defaultValues: { username: '', email: '' },
  });

  return (
    <PixelForm
      form={form}
      onSubmit={(data) => {
        // eslint-disable-next-line no-console
        console.log('submit', data);
      }}
    >
      <PixelForm.Field
        name="username"
        rules={{ required: 'Username is required' }}
        render={({ field }) => (
          <PixelForm.Item>
            <PixelForm.Label>Username</PixelForm.Label>
            <PixelForm.Control>
              <PixelInput placeholder="pxlhero" {...field} />
            </PixelForm.Control>
            <PixelForm.Description>Your retro alias.</PixelForm.Description>
            <PixelForm.Message />
          </PixelForm.Item>
        )}
      />

      <PixelForm.Field
        name="email"
        rules={{
          required: 'Email is required',
          pattern: { value: /.+@.+\..+/, message: 'Enter a valid email' },
        }}
        render={({ field }) => (
          <PixelForm.Item>
            <PixelForm.Label>Email</PixelForm.Label>
            <PixelForm.Control>
              <PixelInput type="email" placeholder="hero@pxlkit.xyz" {...field} />
            </PixelForm.Control>
            <PixelForm.Message />
          </PixelForm.Item>
        )}
      />
    </PixelForm>
  );
}

Examples

Default

import { useForm } from 'react-hook-form';
import { PixelForm, PixelInput } from '@pxlkit/ui-kit';

type DefaultValues = {
  username: string;
  email: string;
};

export function Default() {
  const form = useForm<DefaultValues>({
    defaultValues: { username: '', email: '' },
  });

  return (
    <PixelForm
      form={form}
      onSubmit={(data) => {
        // eslint-disable-next-line no-console
        console.log('submit', data);
      }}
    >
      <PixelForm.Field
        name="username"
        rules={{ required: 'Username is required' }}
        render={({ field }) => (
          <PixelForm.Item>
            <PixelForm.Label>Username</PixelForm.Label>
            <PixelForm.Control>
              <PixelInput placeholder="pxlhero" {...field} />
            </PixelForm.Control>
            <PixelForm.Description>Your retro alias.</PixelForm.Description>
            <PixelForm.Message />
          </PixelForm.Item>
        )}
      />

      <PixelForm.Field
        name="email"
        rules={{
          required: 'Email is required',
          pattern: { value: /.+@.+\..+/, message: 'Enter a valid email' },
        }}
        render={({ field }) => (
          <PixelForm.Item>
            <PixelForm.Label>Email</PixelForm.Label>
            <PixelForm.Control>
              <PixelInput type="email" placeholder="hero@pxlkit.xyz" {...field} />
            </PixelForm.Control>
            <PixelForm.Message />
          </PixelForm.Item>
        )}
      />
    </PixelForm>
  );
}

Related