---
title: Form Control
description: A container component that wraps form inputs with labels, descriptions, and error messages, providing consistent layout and validation state management
---

```tsx
'use client';

import { FormControl } from '@/ui/forms/FormControl';

export function FormControlDemo() {
  return (
    <FormControl
      id="email"
      label="Email Address"
      description="We'll never share your email"
      required
      className="max-w-md"
    >
      <input
        id="email"
        type="email"
        placeholder="Enter your email"
        className="rounded border border-grey px-12 py-8 w-full"
      />
    </FormControl>
  );
}
```

## Installation

Using the CLI:

```bash
npx shadcn@latest add https://develop.trident-ui.pro.clubmed/r/form-control.json
```

Or manually, copy the following code into `/ui/forms/form-control.tsx`:

```tsx
import type { HTMLAttributes, InputHTMLAttributes, ReactNode } from 'react';
import { useInternalStatus, type ValidationStatus } from '../hooks/useInternalStatus';
import { FormLabel, type FormLabelProps } from './FormLabel';
import clsx from 'clsx';
import { FormControlError } from './FormControlError';

export type FormControlProps<
  Value = unknown,
  Attributes extends HTMLAttributes<HTMLElement> = InputHTMLAttributes<HTMLInputElement>,
> = {
  value?: Value;
  label?: ReactNode;
  labelClassName?: string;
  validationStatus?: ValidationStatus;
  onChange?: (event: Event, value: Value) => void;
  /**
   * @deprecated use ["data-testid"] instead
   */
  dataTestId?: string;
  'data-testid'?: string;
  dataName?: string;
  errorMessage?: string;
  inline?: boolean;
} & Omit<Attributes & Partial<FormLabelProps>, 'onChange' | 'value'>;

export const FormControl = <Value = string,>({
  id,
  label,
  description,
  disabled,
  required,
  className,
  validationStatus = 'default',
  children,
  errorMessage,
  dataTestId,
  'data-testid': dataTestIdProp,
  dataName,
  labelClassName,
  hideRequiredStar,
  layout,
  inline,
}: FormControlProps<Value>) => {
  const internalStatus = useInternalStatus({
    isDisabled: !!disabled,
    validationStatus,
  });

  const shouldDisplayErrorMessage = internalStatus === 'error' && errorMessage;

  return (
    <div
      className={clsx('flex gap-4', inline ? '' : 'flex-col', className)}
      data-name={dataName}
      data-testid={dataTestIdProp ?? dataTestId}
    >
      {label && id && (
        <FormLabel
          description={description}
          id={id}
          layout={layout}
          required={required}
          hideRequiredStar={hideRequiredStar}
          className={labelClassName}
        >
          {label}
        </FormLabel>
      )}

      {children}

      {shouldDisplayErrorMessage && <FormControlError>{errorMessage}</FormControlError>}
    </div>
  );
};
```

## Usage

```tsx showLineNumbers
'use client';

import { useState } from 'react';
import { FormControl } from '@/docs/lib/ui/forms/form-control';

export function Component() {
  const [email, setEmail] = useState('');
  const [error, setError] = useState('');

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    setEmail(e.target.value);
    if (!e.target.value.includes('@')) {
      setError('Please enter a valid email address');
    } else {
      setError('');
    }
  };

  return (
    <FormControl
      id="email"
      label="Email Address"
      description="We'll never share your email"
      required
      validationStatus={error ? 'error' : 'default'}
      errorMessage={error}
      className="max-w-md"
    >
      <input
        id="email"
        type="email"
        value={email}
        onChange={handleChange}
        className="rounded border border-grey px-12 py-8 w-full"
      />
    </FormControl>
  );
}
```

## API Reference

### FormControl

| Prop               | Type                                                   | Default     | Description                                                                                               |
| ------------------ | ------------------------------------------------------ | ----------- | --------------------------------------------------------------------------------------------------------- |
| `children`         | `ReactNode`                                            | `undefined` | The form input element(s) to render inside the form control                                               |
| `id`               | `string`                                               | `undefined` | Unique identifier that links the label to the input element. Required when using a label                  |
| `label`            | `ReactNode`                                            | `undefined` | Label text or content to display above the input. Only renders when both `label` and `id` are provided    |
| `labelClassName`   | `string`                                               | `undefined` | Additional CSS classes to apply to the FormLabel component                                                |
| `description`      | `string`                                               | `undefined` | Helper text displayed alongside the label                                                                 |
| `disabled`         | `boolean`                                              | `undefined` | When true, hides error messages and applies disabled state via useInternalStatus                          |
| `required`         | `boolean`                                              | `undefined` | Marks the field as required and displays a required star indicator in the label                           |
| `hideRequiredStar` | `boolean`                                              | `undefined` | When true, hides the required star even when the field is required                                        |
| `layout`           | `"horizontal" \| "vertical" \| "horizontal-${string}"` | `undefined` | Controls the layout orientation of the label. Passed to FormLabel component                               |
| `validationStatus` | `"default" \| "error" \| "success"`                    | `"default"` | Visual validation state of the form control. Use "error" to display error messages                        |
| `errorMessage`     | `string`                                               | `undefined` | Error message to display below the input when `validationStatus` is "error" and the field is not disabled |
| `dataTestId`       | `string`                                               | `undefined` | Custom data-testid attribute for testing purposes                                                         |
| `dataName`         | `string`                                               | `undefined` | Custom data-name attribute for styling and identification                                                 |
| `className`        | `string`                                               | `undefined` | Additional CSS classes to apply to the outer container div                                                |
| `value`            | `Value`                                                | `undefined` | Generic value prop that can be passed through (type parameter, not commonly used directly)                |
| `onChange`         | `(event: Event, value: Value) => void`   | `undefined` | Generic onChange handler signature used across form components                                             |

The FormControl component also inherits props from FormLabelProps and supports generic type parameters for flexible value and attribute types.

## Notes

- **Layout container**: FormControl provides a consistent flex column layout with gap-4 spacing between label, input, and error message
- **Label integration**: Automatically renders a FormLabel component when both `label` and `id` props are provided
- **Conditional error display**: Error messages only appear when `validationStatus` is "error" and the field is not disabled
- **Internal status management**: Uses the `useInternalStatus` hook to coordinate validation status and disabled state. When disabled, the internal status becomes "disabled" which hides error messages
- **Generic type support**: Supports generic type parameters for flexible value types and HTML attribute types, making it reusable across different input types
- **FormLabel delegation**: Passes `description`, `layout`, `required`, `hideRequiredStar`, and `labelClassName` props to the FormLabel component for consistent label rendering
- **Error component**: Uses FormControlError component to display error messages with proper styling and accessibility attributes
- **Data attributes**: Supports custom `data-name` and `data-testid` attributes for styling and testing
- **Composition pattern**: Designed as a composition wrapper that accepts any form input as children, providing flexibility for different input types (text, select, textarea, etc.)
- **Accessibility**: Properly links labels to inputs via the `id` prop, ensuring screen readers can associate labels with their inputs
- **Client component**: This is a client component that requires the `"use client"` directive due to the useInternalStatus hook
