# Input

> A single-line native text input, and TextInput, the field with a label, description, message and counter.

Source: /docs/components/input

```tsx
import { Input, TextInput } from '@aksornproject/aksara-ui'
```

## Labelled field

`TextInput` renders a `<label>` linked to the input, then `description`, the input, `message` and `counter`, in that order. The label gets a generated id unless you pass `id`. `required` sets native `required` and draws a star after the label. The native input owns `value`, `defaultValue`, `onChange` and form submission, and `ref` is the `HTMLInputElement`.

```tsx
import { TextInput } from '@aksornproject/aksara-ui'

export default function InputTextInput() {
  return (
    <div className="w-full max-w-spacing-320">
      <TextInput
        label="Full name"
        description="As it appears on your student card"
        name="fullName"
        placeholder="Somchai Jaidee"
        required
      />
    </div>
  )
}
```

## Status and message

`status` takes `'error'`, `'warning'` or `'success'` and colours the border. `message` renders under the input in the status colour, with its glyph. `'error'` also sets `aria-invalid`, unless you pass your own. `destructive` is a shorthand for `status="error"`. Prefer `status`, which wins when you pass both.

```tsx
import { TextInput } from '@aksornproject/aksara-ui'

export default function InputStatus() {
  return (
    <div className="flex w-full max-w-spacing-320 flex-col gap-spacing-16">
      <TextInput
        label="Email"
        defaultValue="somchai@school"
        status="error"
        message="Enter a full email address."
      />
      <TextInput
        label="Username"
        defaultValue="somchai"
        status="warning"
        message="This name is already common in your class."
      />
      <TextInput
        label="Classroom code"
        defaultValue="M4-2"
        status="success"
        message="Classroom found."
      />
    </div>
  )
}
```

## Adornments

`startAdornment` and `endAdornment` render content in spans before and after the input. An adornment takes the placeholder colour while the input is empty and the primary text colour once it has a value. `endAdornment` is at most 80px wide. `startAdornmentClassName` and `endAdornmentClassName` reach the spans.

```tsx
import { IconInterfaceEssentialSearchLoupe } from '@aksornproject/aksara-icons'
import { TextInput } from '@aksornproject/aksara-ui'

export default function InputAdornments() {
  return (
    <div className="flex w-full max-w-spacing-320 flex-col gap-spacing-16">
      <TextInput
        label="Search lessons"
        type="search"
        placeholder="Search by title"
        startAdornment={<IconInterfaceEssentialSearchLoupe size={20} />}
      />
      <TextInput
        label="Price"
        type="number"
        placeholder="0"
        endAdornment="บาท"
      />
    </div>
  )
}
```

## Clear action and counter

`clearAction` renders in a 36px square slot before `endAdornment`. It is hidden while the input is disabled, read-only or loading. The component never clears the value itself, so the app owns the button's visibility, its accessible name, the value change and where focus goes. `counter` is app-owned text in a right-aligned row. It does not count anything, and `maxLength` passes through to the input unchanged.

```tsx
import { IconInterfaceEssentialDeleteDisabled } from '@aksornproject/aksara-icons'
import { Button, TextInput } from '@aksornproject/aksara-ui'
import { useRef, useState } from 'react'

export default function InputClearAndCounter() {
  const [value, setValue] = useState('วิทยาศาสตร์')
  const ref = useRef<HTMLInputElement>(null)

  return (
    <div className="w-full max-w-spacing-320">
      <TextInput
        ref={ref}
        label="Course name"
        value={value}
        onChange={(event) => setValue(event.target.value)}
        maxLength={100}
        counter={`${value.length} / 100`}
        clearAction={
          value && (
            <Button
              type="button"
              size="xs"
              iconOnly
              variant="tertiary-accent"
              aria-label="Clear course name"
              leadingIcon={<IconInterfaceEssentialDeleteDisabled />}
              onClick={() => {
                setValue('')
                ref.current?.focus()
              }}
            />
          )
        }
      />
    </div>
  )
}
```

## States

`disabled` keeps native disabled behaviour, so the value is left out of form data. `readOnly` uses a grey surface and keeps the text at full strength. `loading` keeps the input mounted with its value, sets `readOnly` and `aria-busy`, and covers the label and value with skeletons. Adornments, `clearAction` and `message` hide while loading. `description` and `counter` stay.

```tsx
import { TextInput } from '@aksornproject/aksara-ui'

export default function InputStates() {
  return (
    <div className="flex w-full max-w-spacing-320 flex-col gap-spacing-16">
      <TextInput label="Disabled" defaultValue="Term 1, 2026" disabled />
      <TextInput label="Read-only" defaultValue="STU-104238" readOnly />
      <TextInput
        label="Loading"
        defaultValue="Somchai Jaidee"
        description="Fetching your profile"
        loading
      />
    </div>
  )
}
```

## Input

`Input` is the control without the field around it. Native props, events and `ref` go to the `<input>`. `className` and `wrapperRef` target the styled `div` around it, and `containerShadowClassName` targets the outer `div`. There are no size variants. `size` is the native attribute. Name a bare `Input` with `aria-label` or an [`InputLabel`](/docs/components/input-label).

```tsx
import { Input } from '@aksornproject/aksara-ui'

export default function InputBare() {
  return (
    <Input
      aria-label="Score"
      type="number"
      inputMode="numeric"
      placeholder="0"
      endAdornment="/ 100"
      containerShadowClassName="w-spacing-160"
    />
  )
}
```

## Accessibility

- `aria-describedby` lists your own ids first, then the description, the message and the counter.
- The required star is `aria-hidden`. The input carries native `required`.
- No live region is added. Announce validation messages yourself if they need it.
- The focus ring draws when the input has keyboard focus. Hover and focus also switch the border to the accent colour, and a status border wins over both.
- Selectors must match attribute values. The input always has `data-disabled` and `data-invalid` as `"true"` or `"false"`.

## API

### TextInput

`TextInput` takes every `Input` prop as well.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `endAdornment` | `ReactNode` |  | Content after the input, such as a unit. Up to 80px wide. Hidden while `loading`. |
| `startAdornment` | `ReactNode` |  | Content before the input, such as an icon. Hidden while `loading`. |
| `startAdornmentClassName` | `string` |  | Classes for the span around `startAdornment`, merged last. |
| `endAdornmentClassName` | `string` |  | Classes for the span around `endAdornment`, merged last. |
| `destructive` | `boolean` | `false` | Shows the `'error'` status when `status` is not set. |
| `wrapperRef` | `Ref<HTMLDivElement>` |  | Ref to the styled `div` around the input. |
| `containerShadowClassName` | `string` |  | Classes for the outer `div`, for sizing. It adds no shadow. |
| `status` | `"error" \| "success" \| "warning"` |  | Colours the border. `'error'` also sets `aria-invalid`. |
| `loading` | `boolean` | `false` | Keeps the value, sets `readOnly` and `aria-busy`, and covers the value with a skeleton. |
| `clearAction` | `ReactNode` |  | A clear button before `endAdornment`. Hidden while disabled, read-only or loading. The app owns what it does. |
| `label` | `ReactNode` |  | Rendered in a `<label>` linked to the input. |
| `description` | `ReactNode` |  | Rendered under the label and linked with `aria-describedby`. |
| `message` | `ReactNode` |  | Rendered under the input in the `status` colour, with its glyph. Linked with `aria-describedby`. Hidden while `loading`. |
| `counter` | `ReactNode` |  | App-owned text in a right-aligned row at the bottom, such as `12 / 100`. Linked with `aria-describedby`. |
| `fieldClassName` | `string` |  | Classes for the outer field stack. |

### Input

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `endAdornment` | `ReactNode` |  | Content after the input, such as a unit. Up to 80px wide. Hidden while `loading`. |
| `startAdornment` | `ReactNode` |  | Content before the input, such as an icon. Hidden while `loading`. |
| `startAdornmentClassName` | `string` |  | Classes for the span around `startAdornment`, merged last. |
| `endAdornmentClassName` | `string` |  | Classes for the span around `endAdornment`, merged last. |
| `destructive` | `boolean` | `false` | Shows the `'error'` status when `status` is not set. |
| `wrapperRef` | `Ref<HTMLDivElement>` |  | Ref to the styled `div` around the input. |
| `containerShadowClassName` | `string` |  | Classes for the outer `div`, for sizing. It adds no shadow. |
| `status` | `"error" \| "success" \| "warning"` |  | Colours the border. `'error'` also sets `aria-invalid`. |
| `loading` | `boolean` | `false` | Keeps the value, sets `readOnly` and `aria-busy`, and covers the value with a skeleton. |
| `clearAction` | `ReactNode` |  | A clear button before `endAdornment`. Hidden while disabled, read-only or loading. The app owns what it does. |