# Input (/docs/input)





`Input` is the lowest layer of the form family — the **shell** that Select, Combobox, Autocomplete, and PillsInput all render into. It draws the box, the focus-within ring, the slots, and the `invalid` / `disabled` states, and nothing else. Labels, descriptions, and errors are the job of [`Field`](/docs/field), so an `Input` stays composable.

<Callout type="info" title="Looking for a labelled text field?">
  Use [`TextInput`](/docs/text-input) — it's this component with `label` /
  `description` / `error` / `required` built in, like every other control in the
  family. `Input` is for composing the shell yourself: inside a `Field` you
  already control, or for a field with no visible label of its own.
</Callout>

```tsx
import { Input, InputBase } from '@42-central/ui-react/input';

<Input placeholder="login@student.42.fr" />
```

Two pieces ship from the module:

* **`Input`** — a real `<input>` wrapped in the shell. Native input props (`value`, `onChange`, `name`, `type`, `ref`, …) forward straight through.
* **`InputBase`** — the bare shell around an arbitrary control child. It's how the rest of the family mounts a `<button>` trigger or a `<div>` of pills inside the same box. Style your control with the exported `inputControlClasses`.

## Playground [#playground]

<story.Playground />

## Variants [#variants]

Two shells. `default` is bordered; `unstyled` strips the chrome but keeps the layout (height + slots) for full-custom inputs.

<Preview align="block" code="&#x22;<Input variant=\&#x22;default\&#x22; placeholder=\&#x22;Default\&#x22; />\n<Input variant=\&#x22;unstyled\&#x22; placeholder=\&#x22;Unstyled\&#x22; />&#x22;">
  <div className="flex w-full max-w-[20rem] flex-col gap-4">
    <Input variant="default" placeholder="Default" />

    <Input variant="unstyled" placeholder="Unstyled" />
  </div>
</Preview>

## Sizes [#sizes]

Five heights — `xs` through `xl`. The control font, padding, and any slot icons scale to match.

<Preview align="block" code="&#x22;<Input size=\&#x22;xs\&#x22; placeholder=\&#x22;xs\&#x22; />\n<Input size=\&#x22;sm\&#x22; placeholder=\&#x22;sm\&#x22; />\n<Input size=\&#x22;md\&#x22; placeholder=\&#x22;md\&#x22; />\n<Input size=\&#x22;lg\&#x22; placeholder=\&#x22;lg\&#x22; />\n<Input size=\&#x22;xl\&#x22; placeholder=\&#x22;xl\&#x22; />&#x22;">
  <div className="flex w-full max-w-[20rem] flex-col gap-3">
    <Input size="xs" placeholder="xs" />

    <Input size="sm" placeholder="sm" />

    <Input size="md" placeholder="md" />

    <Input size="lg" placeholder="lg" />

    <Input size="xl" placeholder="xl" />
  </div>
</Preview>

## Slots [#slots]

`startSlot` and `endSlot` are RTL-aware (start = left in LTR, right in RTL). Icons placed in them are auto-sized to the input's `size`.

Both slots are **decorative**: their wrapper spans are `aria-hidden`, so an interactive control placed in one is invisible to assistive tech. Mount a real button — a clear button, a reveal toggle — as a direct child of [`InputBase`](#inputbase--custom-controls) instead, the way [`PasswordInput`](/docs/password-input) mounts its own.

<Preview align="block" code="&#x22;<Input startSlot={<Search />} placeholder=\&#x22;Search\&#x22; />\n<Input startSlot={<Mail />} endSlot={<CircleX />} placeholder=\&#x22;login@student.42.fr\&#x22; />&#x22;">
  <div className="flex w-full max-w-[20rem] flex-col gap-4">
    <Input startSlot="<Search />" placeholder="Search" />

    <Input startSlot="<Mail />" endSlot="<CircleX />" defaultValue="hi@42.fr" />
  </div>
</Preview>

## States [#states]

`invalid` swaps the border and focus ring to red (and sets `aria-invalid`). `disabled` dims the shell and blocks input; `readOnly` keeps full contrast and stays focusable but rejects edits.

<Preview align="block" code="&#x22;<Input invalid defaultValue=\&#x22;not-an-email\&#x22; />\n<Input disabled defaultValue=\&#x22;Locked\&#x22; />\n<Input readOnly defaultValue=\&#x22;Read-only\&#x22; />&#x22;">
  <div className="flex w-full max-w-[20rem] flex-col gap-4">
    <Input defaultValue="not-an-email" />

    <Input defaultValue="Locked" />

    <Input defaultValue="Read-only" />
  </div>
</Preview>

## InputBase — custom controls [#inputbase--custom-controls]

`InputBase` renders the shell around any control child. This is how a Select trigger (a `<button>`) or a PillsInput (a `<div>`) get the exact same box. Apply `inputControlClasses` to your control so it inherits the shell's font and fills the row.

```tsx
import { InputBase, inputControlClasses } from '@42-central/ui-react/input';

<InputBase pointer endSlot={<ChevronDown />}>
  <button type="button" className={inputControlClasses}>
    {value ?? <span className="text-gray-light-500">Pick a value</span>}
  </button>
</InputBase>
```

## API [#api]

<TypeTable
  type="{
  variant: {
    type: '&#x22;default&#x22; | &#x22;unstyled&#x22;',
    default: '&#x22;default&#x22;',
    description: 'Shell treatment: bordered or chrome-less.',
  },
  size: {
    type: '&#x22;xs&#x22; | &#x22;sm&#x22; | &#x22;md&#x22; | &#x22;lg&#x22; | &#x22;xl&#x22;',
    default: '&#x22;md&#x22;',
    description: 'Height, padding, control font, and slot icon size.',
  },
  invalid: {
    type: 'boolean',
    default: 'false',
    description: 'Red border + ring; sets aria-invalid on the input.',
  },
  disabled: {
    type: 'boolean',
    default: 'false',
    description: 'Dims the shell and disables the input element.',
  },
  readOnly: {
    type: 'boolean',
    default: 'false',
    description: 'Focusable and full-contrast, but the value cannot change.',
  },
  pointer: {
    type: 'boolean',
    default: 'false',
    description: 'cursor: pointer — for button-like triggers (Select).',
  },
  startSlot: {
    type: 'ReactNode',
    description: 'Content at the inline-start (icon, prefix). RTL-aware.',
  },
  endSlot: {
    type: 'ReactNode',
    description: 'Content at the inline-end (chevron, clear button, spinner). RTL-aware.',
  },
  classNames: {
    type: '{ root?, startSlot?, endSlot?, input? }',
    description: 'Per-part class overrides.',
  },
  testId: {
    type: 'string',
    description: 'data-testid on the real control — the inner <input> for Input, the shell <div> itself for InputBase. See the Testing guide.',
  },
}"
/>

`Input` forwards every other prop to the underlying `<input>` (`value`, `defaultValue`, `onChange`, `name`, `type`, `placeholder`, `ref`, …). `className` styles the shell root. `InputBase` takes the same shell props and renders its `children` as the control.

## Accessibility [#accessibility]

* `Input` is an unlabeled control by design. Pair it with [`Field`](/docs/field) (or your own `<label htmlFor>`) to give it an accessible name, description, and error text — `Field` wires the `aria-*` relationships for you.
* `invalid` sets `aria-invalid` on the input so assistive tech announces the error state.
* The focus ring is `focus-within`, so it appears whenever the nested control is focused — including the `<button>` / `<div>` controls mounted via `InputBase`.
* RTL: the shell is a plain flex row, so `startSlot` / `endSlot` and padding flip automatically under `dir="rtl"`.
