MDK Logo

Form composition

Form provider and compound parts for building custom form fields

The <Form> provider plus the seven compound parts (FormField, FormItem, FormLabel, FormControl, FormDescription, FormMessage) are the building blocks for any form. Use them when no prebuilt field fits, or when you need full control over a field's layout.

For standalone inputs that you can drop in without <Form>, see Controls.

Prerequisites

Components

ComponentDescription
FormForm wrapper with validation and submission handling
FormControlSlot wrapper that wires ARIA attributes onto a control
FormDescriptionHelper text paragraph rendered below a field
FormFieldController wrapper that provides field context to descendants
FormItemLayout wrapper grouping a label, control, description, and message
FormLabelLabel that auto-links to the form field input
FormMessageValidation message paragraph for a form field

Form

Form wrapper with validation and submission handling built on react-hook-form.

The package includes a complete form system built on React hook form. <Form> is the context provider; the other six components on this page are the building blocks you compose inside it. See Prebuilt fields for ready-made wrappers that render a labelled, validated field in a single tag.

Import

import {
  Form,
  FormField,
  FormItem,
  FormLabel,
  FormControl,
  FormDescription,
  FormMessage,
  useFormField,
} from '@mdk/core'

Props

PropTypeDefaultDescription
formUseFormReturnrequiredThe instance returned by useForm()
childrenReactNoderequiredForm content
onSubmitFormEventHandlernoneSubmit handler. Wrap with form.handleSubmit(...) to get parsed values

Any other standard <form> element attribute (id, action, noValidate, onReset, ref, data-*, aria-*, etc.) is forwarded to the underlying DOM element.

Basic usage

import { useForm } from 'react-hook-form'
import { Form, FormField, FormItem, FormLabel, FormControl, FormMessage, Input, Button } from '@mdk/core'

function MyForm() {
  const form = useForm({
    defaultValues: { email: '' },
  })

  const onSubmit = (values) => {
    console.log(values)
  }

  return (
    <Form form={form} onSubmit={form.handleSubmit(onSubmit)}>
      <FormField
        control={form.control}
        name="email"
        render={({ field }) => (
          <FormItem>
            <FormLabel>Email</FormLabel>
            <FormControl>
              <Input placeholder="email@example.com" {...field} />
            </FormControl>
            <FormMessage />
          </FormItem>
        )}
      />
      <Button type="submit">Submit</Button>
    </Form>
  )
}

Built-in validators

The package re-exports common Zod schemas and validator helpers so you can wire useForm to a resolver without rolling your own.

import { validators, loginSchema, registerSchema } from '@mdk/core'

Styling

  • .mdk-form: Root <form> element

FormField

Wraps react-hook-form's Controller and provides field context to descendants so FormItem, FormLabel, FormControl, and FormMessage can read field state.

Import

import { FormField } from '@mdk/core'

Props

FormField is a thin wrapper over React hook form's Controller and accepts the same props (control, name, defaultValue, rules, render, etc.). It additionally provides field context to descendants like FormLabel and FormControl so they can wire up ARIA attributes automatically.

Basic usage

<FormField
  control={form.control}
  name="username"
  render={({ field }) => (
    <FormItem>
      <FormLabel>Username</FormLabel>
      <FormControl>
        <Input {...field} />
      </FormControl>
      <FormMessage />
    </FormItem>
  )}
/>

FormItem

Layout wrapper for a form field. Generates a unique id used by FormLabel, FormControl, FormDescription, and FormMessage for accessibility linking.

Import

import { FormItem } from '@mdk/core'

Props

PropTypeDefaultDescription
classNamestringnoneExtra classes merged onto the wrapper

Any other standard <div> attribute (id, ref, data-*, aria-*, etc.) is forwarded to the underlying DOM element.

Basic usage

<FormField
  control={form.control}
  name="bio"
  render={({ field }) => (
    <FormItem>
      <FormLabel>Bio</FormLabel>
      <FormControl>
        <TextArea {...field} />
      </FormControl>
      <FormDescription>Max 200 characters</FormDescription>
      <FormMessage />
    </FormItem>
  )}
/>

Styling

  • .mdk-form-item: Root element

FormLabel

Label that auto-links to the form field input via generated ids. Applies error styling when the field has a validation error.

Import

import { FormLabel } from '@mdk/core'

Props

Accepts every prop the underlying Label accepts. The htmlFor attribute is set automatically from the FormItem context, and an error modifier class is applied when the field has a validation error.

Basic usage

<FormItem>
  <FormLabel>Email address</FormLabel>
  <FormControl>
    <Input type="email" {...field} />
  </FormControl>
</FormItem>

Styling

  • .mdk-form-label: Root element
  • .mdk-form-label--error: Applied when the field has a validation error

FormControl

Slot-based wrapper that injects id, aria-describedby, and aria-invalid onto its child input element without adding an extra DOM wrapper.

Import

import { FormControl } from '@mdk/core'

Props

FormControl uses Radix Slot to merge its props onto its single child without adding an extra DOM node. It autoinjects id, aria-describedby, and aria-invalid based on the surrounding FormItem/FormField context.

Basic usage

<FormItem>
  <FormLabel>Phone</FormLabel>
  <FormControl>
    <Input type="tel" {...field} />
  </FormControl>
  <FormMessage />
</FormItem>

FormControl must wrap exactly one child element.

FormDescription

Optional helper text paragraph displayed below the input. Auto-linked to the control via aria-describedby.

Import

import { FormDescription } from '@mdk/core'

Props

PropTypeDefaultDescription
classNamestringnoneExtra classes merged onto the wrapper

Any other standard <p> attribute (id, ref, data-*, aria-*, etc.) is forwarded to the underlying DOM element.

Basic usage

<FormItem>
  <FormLabel>Password</FormLabel>
  <FormControl>
    <Input type="password" {...field} />
  </FormControl>
  <FormDescription>Must be at least 8 characters</FormDescription>
  <FormMessage />
</FormItem>

Styling

  • .mdk-form-description: Root element

FormMessage

Displays the validation error message from react-hook-form field state. Falls back to children when no error is present and always renders to prevent layout shift.

Import

import { FormMessage } from '@mdk/core'

Props

PropTypeDefaultDescription
childrenReactNodenoneFallback content shown when the field has no validation error
classNamestringnoneExtra classes merged onto the wrapper

Any other standard <p> attribute (id, ref, data-*, aria-*, etc.) is forwarded to the underlying DOM element.

FormMessage always renders to prevent layout shift. When an error is present it shows the validation message and applies role="alert"; otherwise it shows children (or a non-breaking space placeholder) and adds the --empty modifier class.

Basic usage

<FormItem>
  <FormLabel>Email</FormLabel>
  <FormControl>
    <Input type="email" {...field} />
  </FormControl>
  <FormMessage />
</FormItem>

Styling

  • .mdk-form-message: Root element
  • .mdk-form-message--empty: Applied when no error and no children are present

On this page