# Text Field

A text field takes one line of input, such as a name, an email address, a search term or a number.

A text field takes one line of text. Slots on either side hold an icon, a unit or a small button. For text that runs over several lines, use a [Text Area](/docs/text-area). To pick from a fixed list, use a [Select](/docs/select) or a [Combobox](/docs/combobox).


## Usage

```tsx
import { Flex, Text, TextField } from '@kushagradhawan/kookie-ui';

export function EmailField({ error }: { error?: string }) {
  return (
    <Flex direction="column" gap="1">
      <Text as="label" htmlFor="email" size="2" weight="medium">
        Email address
      </Text>
      <TextField.Root
        id="email"
        type="email"
        autoComplete="email"
        error={Boolean(error)}
        errorMessage={error}
      />
    </Flex>
  );
}
```

## Anatomy

```tsx
<TextField.Root>
  <TextField.Slot />
  <TextField.Slot side="right" />
</TextField.Root>

<TextField.Group>
  <TextField.Root />
  <TextField.Root />
</TextField.Group>
```

- **Root:** The input and the box around it. Props on Root go to the `input` element.
- **Slot:** Content before or after the input, such as an icon, a unit or a button.
- **Group:** Two or more fields as one control, such as X, Y and Z.

## Best practices

**Give every field a visible label.** Put the label above the field and connect it with `htmlFor` or `aria-labelledby`. A placeholder disappears as soon as someone types, so it can't stand in for the label.

**Size the field to the answer.** A postcode field and an address field shouldn't be the same width. The width hints at how much to type.

**Say what is wrong and how to fix it.** Set `error` and pass an `errorMessage` such as "Enter an email address like name@example.com". "Invalid input" leaves people guessing.

**Set the right `type` and `autoComplete`.** `type="email"` or `type="tel"` brings up the matching keyboard on phones, and `autoComplete` lets the browser fill in what it already knows.

**Use a Text Area for long answers.** A text field scrolls sideways when the text runs past its edge, so people can't see what they wrote. Messages, descriptions and addresses belong in a [Text Area](/docs/text-area).

## Variant

`variant` sets the look. All of them share one height, so they can sit in the same form.

| Variant | Use it for |
| --- | --- |
| `raised` | Most forms. This is the default |
| `surface` | Fields on a plain background that need a clear edge |
| `soft` | Fields inside a card or a panel that already has a border |
| `outline` | Dense forms where a fill would feel heavy |
| `ghost` | Inline editing, where the field should look like text until focused |

## Size

`size` picks one of four heights: 28, 32, 40 and 48 pixels. Size 2 is the default. A text field is as tall as a [Button](/docs/button) or a [Select](/docs/select) of the same size, so a search field and its button line up. `size` accepts a responsive object, such as `{ initial: '3', md: '2' }`.

## Examples

### Search

Put the icon in a left slot. Clicking a slot focuses the input, so the whole box acts as one target.

```tsx
<TextField.Root type="search" placeholder="Search projects" aria-label="Search projects">
  <TextField.Slot>
    <HugeiconsIcon icon={Search01Icon} strokeWidth={1.75} />
  </TextField.Slot>
</TextField.Root>
```

### Units

A right slot holds the unit, so people type only the number.

```tsx
<TextField.Root type="number" defaultValue="16" aria-label="Font size">
  <TextField.Slot side="right">
    <Text size="2" emphasis="medium">px</Text>
  </TextField.Slot>
</TextField.Root>
```

### Dragging to change a number

`scrub` on a slot changes a number when someone drags across the slot, like the property fields in a design tool. `onScrub` receives the change, and `scrubMin` and `scrubMax` keep the result in range when you pass the current value as `scrubValue`. Hold Shift for steps 10 times larger and Alt for steps 10 times smaller.

```tsx
const [width, setWidth] = React.useState(320);

<TextField.Root
  type="number"
  value={width}
  onChange={(event) => setWidth(Number(event.target.value) || 0)}
  aria-label="Width"
>
  <TextField.Slot
    scrub
    scrubValue={width}
    scrubMin={0}
    scrubMax={2000}
    onScrub={(delta) => setWidth((value) => value + delta)}
  >
    <Text size="2" emphasis="medium">W</Text>
  </TextField.Slot>
</TextField.Root>
```

### Three values in one control

`TextField.Group` draws one edge around its fields and a hairline between them. Its `variant`, `size`, `radius`, `color` and `disabled` pass to every field. `ghost` keeps only the hairlines, for a dense panel.

```tsx
<TextField.Group aria-label="Position">
  <TextField.Root defaultValue="0" aria-label="X">
    <TextField.Slot><Text size="2" color="red">X</Text></TextField.Slot>
  </TextField.Root>
  <TextField.Root defaultValue="50" aria-label="Y">
    <TextField.Slot><Text size="2" color="grass">Y</Text></TextField.Slot>
  </TextField.Root>
  <TextField.Root defaultValue="0" aria-label="Z">
    <TextField.Slot><Text size="2" color="blue">Z</Text></TextField.Slot>
  </TextField.Root>
</TextField.Group>
```

### Validation

`error` marks the field as invalid and turns it red. `errorMessage` shows below the field and is read with it. Show the error after someone leaves the field or submits the form, not while they are still typing.

## Props

### Root

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `'raised' \| 'surface' \| 'soft' \| 'outline' \| 'ghost'` | `'raised'` | The look |
| `size` | `'1' \| '2' \| '3' \| '4'`, responsive | `'2'` | The height step |
| `color` | `AccentColor` | Theme accent | The colour of the focus ring |
| `radius` | `'none' \| 'small' \| 'medium' \| 'large' \| 'full'` | Theme radius | The corner |
| `type` | `'text' \| 'email' \| 'password' \| 'search' \| 'tel' \| 'url' \| 'number' \| 'date' \| 'time' \| 'datetime-local' \| 'month' \| 'week' \| 'hidden'` | `'text'` | The input type |
| `value` | `string \| number` | | The value, when you control it |
| `defaultValue` | `string \| number` | | The starting value, when the field controls it |
| `error` | `boolean` | `false` | Marks the field as invalid |
| `errorMessage` | `string` | | Text shown below the field and linked to it |
| `required` | `boolean` | `false` | Must be filled before the form submits |

Root also accepts every attribute of the HTML `input` element, such as `placeholder`, `disabled`, `readOnly`, `autoComplete` and `onChange`. Spell check follows the browser unless you set `spellCheck`.

### Group

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `variant` | `'raised' \| 'surface' \| 'soft' \| 'outline' \| 'ghost'` | `'raised'` | The look of every field. `ghost` is the hairlines alone |
| `size` | `'1' \| '2' \| '3' \| '4'`, responsive | `'2'` | The height step |
| `color` | `AccentColor` | Theme accent | The colour of the focus ring |
| `radius` | `'none' \| 'small' \| 'medium' \| 'large' \| 'full'` | Theme radius | The outer corners |
| `disabled` | `boolean` | `false` | Disables every field |

A field in the group can still take its own `color`, `disabled` and `error`.

### Slot

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `side` | `'left' \| 'right'` | | Which side of the input. Without it, the first slot goes left and later ones go right |
| `color` | `AccentColor` | | The colour of the slot's content |
| `gap` | Space scale, responsive | | The space between items in the slot |
| `px`, `pl`, `pr` | Space scale, responsive | | The slot's side padding |
| `scrub` | `boolean` | `false` | Changes a number when someone drags across the slot |
| `scrubValue` | `number` | | The current value. Needed for `scrubMin` and `scrubMax` |
| `scrubStep` | `number` | `1` | How much one step changes the value |
| `scrubSensitivity` | `number` | `1` | Pixels of dragging per step. Higher is slower |
| `scrubMin` | `number` | | The lowest value |
| `scrubMax` | `number` | | The highest value |
| `scrubShiftMultiplier` | `number` | `10` | Step multiplier while Shift is held |
| `scrubAltMultiplier` | `number` | `0.1` | Step multiplier while Alt is held |
| `onScrub` | `(delta: number, isChanging: boolean) => void` | | Called with each change. `isChanging` is `false` once the drag ends |

Slot also accepts every attribute of the HTML `div` element.

## Accessibility

| Key | Action |
| --- | --- |
| Tab | Moves focus into the field |
| Typing | Edits the value |

- **Label:** The field renders no label of its own. Connect a visible label with `htmlFor` and `id`, or pass `aria-label` when the purpose is clear from what is around it, as in a search bar.
- **Errors:** `error` sets `aria-invalid`. The `errorMessage` element is linked with `aria-describedby` and has `role="alert"`, so a screen reader reads it when it appears.
- **Slots:** Clicking a slot moves focus to the input, with the cursor at the start for a left slot and at the end for a right slot. A button, link, select, label or other focusable control inside a slot keeps its own focus. A slot of text is linked to the input with `aria-describedby`, so a screen reader reads a unit like "USD" with the field. A slot that holds a control, such as a show-password button, is not.
- **Scrubbing:** Dragging works with a pointer only. Keyboard users type the number in the input, so keep the input editable.
- **Focus:** The focus ring wraps the whole box, slots included.
- **High contrast:** In forced colors mode the field draws a real border, since the browser drops its shadows.
