Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.refactkit.com/llms.txt

Use this file to discover all available pages before exploring further.

RefactKit enforces a strict form composition pattern built on two complementary libraries: TanStack Form for state management and validation, and Base UI primitives for the markup. This separation keeps business logic in one place and visual structure in another — and it ensures every form field is accessible by default through proper ARIA attributes and semantic HTML.

The composition pattern

Every form in RefactKit uses the same stack of components:
ComponentRole
<FieldGroup>Container that groups related fields with consistent spacing
<Field>Wrapper for a single field; receives data-invalid for styling
<FieldLabel>Accessible label linked to the input via htmlFor
<Input>The text input; receives aria-invalid for screen readers
<FieldDescription>Renders hint text or error messages below the input
Import all of these from @/components/ui:
import { FieldGroup, Field, FieldLabel, Input, FieldDescription } from '@/components/ui'

Complete working example

The pattern below is the canonical form implementation in RefactKit. Study the placement of data-invalid on <Field> and aria-invalid on <Input> — both must be set for full accessibility coverage:
import { useForm } from '@tanstack/react-form'
import { FieldGroup, Field, FieldLabel, Input, FieldDescription } from '@/components/ui'

export function ProfileForm() {
  const form = useForm({ defaultValues: { email: '' } })

  return (
    <form
      onSubmit={(e) => {
        e.preventDefault()
        form.handleSubmit()
      }}
    >
      <FieldGroup>
        <form.Field name="email">
          {(field) => (
            <Field data-invalid={field.state.meta.errors.length > 0}>
              <FieldLabel htmlFor={field.name}>Email</FieldLabel>
              <Input
                id={field.name}
                value={field.state.value}
                onChange={(e) => field.handleChange(e.target.value)}
                aria-invalid={field.state.meta.errors.length > 0}
              />
              {field.state.meta.errors.map((err) => (
                <FieldDescription key={err}>{err}</FieldDescription>
              ))}
            </Field>
          )}
        </form.Field>
      </FieldGroup>
    </form>
  )
}

Add validation

Pass a validators object to form.Field to attach validation rules. TanStack Form calls onChange validators on every keystroke and onBlur validators when the field loses focus:
<form.Field
  name="email"
  validators={{
    onChange: ({ value }) =>
      !value
        ? 'Email is required'
        : !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)
          ? 'Enter a valid email address'
          : undefined,
  }}
>
  {(field) => (
    <Field data-invalid={field.state.meta.errors.length > 0}>
      <FieldLabel htmlFor={field.name}>Email</FieldLabel>
      <Input
        id={field.name}
        type="email"
        value={field.state.value}
        onChange={(e) => field.handleChange(e.target.value)}
        onBlur={field.handleBlur}
        aria-invalid={field.state.meta.errors.length > 0}
      />
      {field.state.meta.errors.map((err) => (
        <FieldDescription key={err}>{err}</FieldDescription>
      ))}
    </Field>
  )}
</form.Field>
Return undefined from a validator when the value is valid. TanStack Form treats any non-undefined return value as an error message.

Form-level configuration

Configure default values, async validation, and submission handling on the useForm call:
const form = useForm({
  defaultValues: {
    name: '',
    email: '',
  },
  onSubmit: async ({ value }) => {
    await myServerFn({ data: value })
  },
})
Read form.state.isSubmitting to disable the submit button during in-flight requests:
<Button type="submit" disabled={form.state.isSubmitting}>
  {form.state.isSubmitting ? 'Saving...' : 'Save changes'}
</Button>

Multi-field forms

Wrap multiple fields in a single <FieldGroup>. Do not use <div> tags with arbitrary spacing utilities — <FieldGroup> provides the correct vertical rhythm automatically:
<FieldGroup>
  <form.Field name="name">
    {(field) => (
      <Field data-invalid={field.state.meta.errors.length > 0}>
        <FieldLabel htmlFor={field.name}>Name</FieldLabel>
        <Input
          id={field.name}
          value={field.state.value}
          onChange={(e) => field.handleChange(e.target.value)}
          aria-invalid={field.state.meta.errors.length > 0}
        />
        {field.state.meta.errors.map((err) => (
          <FieldDescription key={err}>{err}</FieldDescription>
        ))}
      </Field>
    )}
  </form.Field>

  <form.Field name="email">
    {(field) => (
      <Field data-invalid={field.state.meta.errors.length > 0}>
        <FieldLabel htmlFor={field.name}>Email</FieldLabel>
        <Input
          id={field.name}
          type="email"
          value={field.state.value}
          onChange={(e) => field.handleChange(e.target.value)}
          aria-invalid={field.state.meta.errors.length > 0}
        />
        {field.state.meta.errors.map((err) => (
          <FieldDescription key={err}>{err}</FieldDescription>
        ))}
      </Field>
    )}
  </form.Field>
</FieldGroup>

Validation state: data-invalid and aria-invalid

RefactKit uses two complementary attributes to communicate invalid state:
  • data-invalid on <Field> — drives CSS styling via Tailwind’s data-[invalid=true]: variant. The field label, border, and background can all change color when this attribute is present.
  • aria-invalid on <Input> — signals to screen readers and assistive technologies that the field contains an error.
Both must be set together. Setting only one means sighted users see the error styling but screen reader users do not hear the announcement, or vice versa.
Never use generic <div> containers with arbitrary spacing classes like space-y-4 to structure form fields. Always use <FieldGroup> and <Field> — this enforces consistent visual rhythm across the app and ensures the Base UI styling system can apply its validation styles correctly.

Available UI components

Beyond the form-specific primitives, @/components/ui exports a full set of Base UI components you can use inside forms:
  • Select, SelectContent, SelectItem, SelectTrigger, SelectValue — dropdown selectors
  • Checkbox — boolean toggles
  • Label — standalone label for non-TanStack-Form usage
  • Button — submit and cancel actions
  • Spinner — inline loading indicator for async submissions
  • Avatar, AvatarImage, AvatarFallback — user/org image display