# Form control

> A label-plus-control compound that threads `id` / `htmlFor` / `aria-describedby` across its slots. Wraps Input, Textarea, or a custom field.

- Category: form
- Status: stable (since 0.1.0)
- Tokens: --foreground-primary, --foreground-muted, --danger-foreground
- Playground: https://design.freecodecamp.org/playground#form-control
- npm dependencies: `react@>=18 <20`
- Registry dependencies: [theme](https://design.freecodecamp.org/registry/theme.md)
- Files:
  - `FormControl.tsx` → `src/ui/form-control/FormControl.tsx` (raw: https://design.freecodecamp.org/registry/form-control/FormControl.tsx)
  - `form-control.css` → `src/ui/form-control/form-control.css` (raw: https://design.freecodecamp.org/registry/form-control/form-control.css)

## Install (copy source)

1. Ensure the theme is installed once per project - tokens.css + base.css imported globally, fonts available. See https://design.freecodecamp.org/registry/theme.md and https://design.freecodecamp.org/registry/starter.md.
2. Copy the files below into `src/ui/form-control/` (adjust to your project layout) and import the CSS once from your global stylesheet, e.g. `@import './ui/form-control/form-control.css';`.
3. Colors, spacing and type come from tokens - tailor the component by editing the copied source; recolour by editing tokens.css, not the component CSS.

## Usage

`<FormControl>` wires the label ↔ control ↔ help relationship so
consumers don't hand-generate ids. Drop an `<Input>`, `<Textarea>`, or
render prop inside and the a11y plumbing flows automatically.

## Accessibility

Generates deterministic ids via `useId()` and wires `htmlFor` on the
label plus `aria-describedby` on the control. When `error` is present,
the control also gains `aria-invalid="true"`. Screen readers announce
the label, the current value, and the help/error text in order.

## Example

```tsx
import { FormControl } from './ui/form-control/FormControl';

<FormControl placeholder="Search the curriculum" />
```

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `className` | `string` | no | - |  |
| `invalid` | `boolean` | no | - |  |
| `as` | `enum` | no | - |  |

## Source: FormControl.tsx

```tsx
import React, { forwardRef } from 'react';

type CommonProps = {
  className?: string;
  invalid?: boolean;
};

type InputBased = CommonProps &
  Omit<React.InputHTMLAttributes<HTMLInputElement>, 'as'> & {
    as?: 'input';
  };

type TextareaBased = CommonProps &
  Omit<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'as'> & {
    as: 'textarea';
  };

export type FormControlProps = InputBased | TextareaBased;

export const FormControl = forwardRef<
  HTMLInputElement | HTMLTextAreaElement,
  FormControlProps
>((props, ref) => {
  const {
    as = 'input',
    className = '',
    invalid,
    ...rest
  } = props as CommonProps & {
    as?: 'input' | 'textarea';
    [key: string]: unknown;
  };
  const isTextarea = as === 'textarea';
  const classes = ['input', isTextarea && 'input--textarea', className]
    .filter(Boolean)
    .join(' ');
  const ariaInvalid = invalid || undefined;
  if (isTextarea) {
    return (
      <textarea
        ref={ref as React.Ref<HTMLTextAreaElement>}
        className={classes}
        aria-invalid={ariaInvalid}
        {...(rest as React.TextareaHTMLAttributes<HTMLTextAreaElement>)}
      />
    );
  }
  return (
    <input
      ref={ref as React.Ref<HTMLInputElement>}
      className={classes}
      aria-invalid={ariaInvalid}
      {...(rest as React.InputHTMLAttributes<HTMLInputElement>)}
    />
  );
});
FormControl.displayName = 'FormControl';
```

## Source: form-control.css

```css
.form-label {
  font-family: var(--font-mono);
  font-size: 13px;
  font-weight: 700;
  letter-spacing: 0.04em;
  text-transform: uppercase;
  color: var(--foreground-tertiary);
}
```

## HTML / vanilla variant

```html
<input class="input" placeholder="Search the curriculum" />
```

Interactive behaviours for plain HTML come from the vanilla runtime (data-uikit-* attributes): https://design.freecodecamp.org/registry/vanilla.md - or download https://design.freecodecamp.org/cdn/uikit.global.js once and self-host it (do not hotlink).

## For coding agents

This library is distributed as copyable source, not an npm package. Start at https://design.freecodecamp.org/registry/starter.md, discover components via https://design.freecodecamp.org/llms.txt, and copy files into the consuming project. Keep token names intact; recolour by editing the copied tokens.css.
