# Input

> A text field for forms, with states for valid and invalid. CSS component.

Plain CSS on an ordinary `<input>`, so the browser’s own validation, autofill and keyboard handling work as before.

## How to use it

**Plain HTML**

```html
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/tokens/tokens.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/input/input.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/src/components/css/label/label.css">
<!-- Or everything in one file: https://cdn.jsdelivr.net/npm/@lo-tek/interfaces@0.36.0/dist/lotek.css -->
```

```html
<label class="lt-label" for="email">Email address</label>
<input class="lt-input" id="email" type="email" placeholder="alex&#64;example.com" />


<label class="lt-label" for="confirm">Confirm email address</label>
<input class="lt-input" id="confirm" type="email" data-state="success" value="alex&#64;example.com" />


<label class="lt-label" for="backup">Backup email</label>
<input class="lt-input" id="backup" type="email" data-state="invalid" aria-invalid="true" value="alex&#64;" />


<label class="lt-label" for="registered">Registered email address</label>
<input class="lt-input" id="registered" type="email" value="alex&#64;example.com" disabled />
```

**TypeScript**

```ts
import "@lo-tek/interfaces/tokens.css"
import "@lo-tek/interfaces/input.css"
import "@lo-tek/interfaces/label.css"
```

`lt` is used in two ways, and which one depends on whether you create the element or find it.

**If you create the markup,** you spread the attributes in. That is the route in React, Astro and anything else that renders:

```tsx
import { lt } from "@lo-tek/interfaces/react"


<input {...lt.input({ type: "email", state: "invalid" })} id="email" />
```

The import goes to `/react` here, since the block is JSX. From there the keys are called `className` and `htmlFor`. In Astro and anything else without React you import `@lo-tek/interfaces` and get `class` and `for`.

**If the element is already there,** you set them on it. That is the route when you enhance HTML a server or a template has sent:

```ts
import { lt, type FieldState } from "@lo-tek/interfaces"


const field = document.querySelector<HTMLInputElement>("#email")
if (!field) throw new Error("field not found")


lt.setAttributes(field, lt.input({ type: "email" }))
```

`type` sets both the HTML type and the icon variant, so they cannot get out of step. With `lt.input()` that trap cannot be walked into: a value that does not exist is a type error.

On validation you call it again with the new state:

```ts
field.addEventListener("blur", () => {
  const state: FieldState = field.validity.valid ? "default" : "invalid"


  lt.setAttributes(field, lt.input({ type: "email", state }))
})
```

`state: "invalid"` sets both `data-state` for the colour and `aria-invalid` for the screen reader. When the field goes back to `"default"`, `lt.setAttributes` removes both. Without that, the field would stay red after the error was corrected.

Values from outside: `lt.input.isType(x)` and `lt.input.isState(x)`. See [Type-safe use](/type-safe-use.md).

| `data-state` | Used for |
| - | - |
| *(omitted)* | Normal state |
| `invalid` | Validation has failed |
| `success` | The value is confirmed valid |

`data-state` is colour. Screen readers also need `aria-invalid="true"`, or [Field](/components/field.md), which sets both.

## Date and time

**Plain HTML**

```html
<label class="lt-label" for="date">Travel date</label>
<input class="lt-input" id="date" data-variant="date" type="date" value="2026-05-31" />


<label class="lt-label" for="time">Departure time</label>
<input class="lt-input" id="time" data-variant="time" type="time" value="14:30" />
```

> **data-variant and type must match**
>
> The icon is controlled by `data-variant`, the picker by `type`. If you set only `data-variant`, you get a field that looks like a date field, but where nothing happens when the user presses the icon. If you set only `type`, the browser’s own button remains, and it works.
>
> `lt.input({ type: "date" })` sets both, and removes the mistake entirely.

Only `date`, `time` and `datetime-local` affect the appearance. The other types exist so that the TypeScript helper can set `type` for you.

## Which date component?

| Situation | Component |
| - | - |
| The browser’s own date picker is enough | **Input** with `type="date"` |
| You need to ask for a date | [Dates in a form](/patterns/date.md) |

## Classes you can style

| Class | What it is |
| - | - |
| `.lt-input` | The field. The state comes from `data-state`, the icon from `data-variant`. |

| Variable | Default |
| - | - |
| `--lt-input-width` | `100%` |
| `--lt-input-padding` | `var(--lt-spacing-2) var(--lt-spacing-3)` |
| `--lt-input-radius` | `var(--lt-spacing-1)` |

An example. A form to be filled in on a phone can take larger touch targets, and your organisation may want square corners:

**Plain HTML**

```html
<label class="lt-label" for="default">Customer number</label>
<input class="lt-input" id="default" type="text" />


<div class="roomy-form">
<label class="lt-label" for="roomy">Customer number</label>
<input class="lt-input" id="roomy" type="text" />
</div>
```

```css
.roomy-form {
--lt-input-padding: var(--lt-spacing-3) var(--lt-spacing-4);
--lt-input-radius: 0;
}
```

The variable is on the container, not on the field, so it applies to every field in the form without you touching any of them.

See [Customising](/customising.md) for how the variables and the layer work.

## Accessibility

- Every field needs a visible `<label>` with `for`. `placeholder` is no replacement, since it disappears as soon as the user types.
- Use the right `type`. It decides the phone keyboard and what autofill the browser offers.
- Set `autocomplete` on fields that ask for personal details, for example `autocomplete="email"`.
