> ## Documentation Index
> Fetch the complete documentation index at: https://docs.awaitio.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Input components

Input components ask a person for a value in an action: `await io.input.text('Company name')` waits until someone answers. `io` offers exactly the components you register. Each one is its own package, named after its method: `io.input.text()` comes from `@awaitio/input-text`. Components that only show something in a page are [display components](/core/display-components).

Install the ones you use, or [write your own](#write-your-own), and register them in `components` when you create the app:

```ts app.ts {7} theme={null}
import { inputText } from '@awaitio/input-text';

export const app = new AwaitIO({
  // name, actionsDirectory, theme, secret, users…
  components: {
    input: {
      text: inputText,
    },
  },
});
```

## Available components

Each card is one package and opens its page: all its props, what it returns and how the server checks the answer.

<Columns cols={2}>
  <Card title="Input Text" href="/io/input-text" img="https://mintcdn.com/awaitio/3lgKY19KdaqwkLKs/images/components/input-text.png?fit=max&auto=format&n=3lgKY19KdaqwkLKs&q=85&s=56890b8a936e0739822b8b82d04e49d7" width="1200" height="750" data-path="images/components/input-text.png">
    `io.input.text()` asks for one line of text and returns a `string`.
  </Card>

  <Card title="Input Email" href="/io/input-email" img="https://mintcdn.com/awaitio/MjbT5AW66vMXbVck/images/components/input-email.png?fit=max&auto=format&n=MjbT5AW66vMXbVck&q=85&s=c82a411a0d2b34059db425b02629aa96" width="1200" height="750" data-path="images/components/input-email.png">
    `io.input.email()` asks for an email address and returns it as a `string`.
  </Card>

  <Card title="Input URL" href="/io/input-url" img="https://mintcdn.com/awaitio/MjbT5AW66vMXbVck/images/components/input-url.png?fit=max&auto=format&n=MjbT5AW66vMXbVck&q=85&s=188d50e218510f19dbac5929339d2b39" width="1200" height="750" data-path="images/components/input-url.png">
    `io.input.url()` asks for a web address and returns a `URL`.
  </Card>

  <Card title="Input Textarea" href="/io/input-textarea" img="https://mintcdn.com/awaitio/MjbT5AW66vMXbVck/images/components/input-textarea.png?fit=max&auto=format&n=MjbT5AW66vMXbVck&q=85&s=7ceeb5855ef5e210c2df39f8e375e744" width="1200" height="750" data-path="images/components/input-textarea.png">
    `io.input.textarea()` asks for longer text over a few lines and returns a `string`.
  </Card>

  <Card title="Input Number" href="/io/input-number" img="https://mintcdn.com/awaitio/MjbT5AW66vMXbVck/images/components/input-number.png?fit=max&auto=format&n=MjbT5AW66vMXbVck&q=85&s=b47c2f92a63ae3ebcc0da1a693a614f9" width="1200" height="750" data-path="images/components/input-number.png">
    `io.input.number()` asks for a number with optional limits and returns a `number`.
  </Card>

  <Card title="Input Boolean" href="/io/input-boolean" img="https://mintcdn.com/awaitio/MjbT5AW66vMXbVck/images/components/input-boolean.png?fit=max&auto=format&n=MjbT5AW66vMXbVck&q=85&s=ccd3f38e25b862b6baf97808368dabcf" width="1200" height="750" data-path="images/components/input-boolean.png">
    `io.input.boolean()` asks a yes or no question with a checkbox and returns a `boolean`.
  </Card>

  <Card title="Input Datetime" href="/io/input-datetime" img="https://mintcdn.com/awaitio/MjbT5AW66vMXbVck/images/components/input-datetime.png?fit=max&auto=format&n=MjbT5AW66vMXbVck&q=85&s=40604ef3327939b72c5015c6eb2435fd" width="1200" height="750" data-path="images/components/input-datetime.png">
    `io.input.datetime()` asks for a date and a time and returns a luxon `DateTime`.
  </Card>

  <Card title="Input Date" href="/io/input-date" img="https://mintcdn.com/awaitio/MjbT5AW66vMXbVck/images/components/input-date.png?fit=max&auto=format&n=MjbT5AW66vMXbVck&q=85&s=cc85e168b74838e6052c8addbada9373" width="1200" height="750" data-path="images/components/input-date.png">
    `io.input.date()` asks for a calendar date and returns a luxon `DateTime`.
  </Card>

  <Card title="Select Single" href="/io/select-single" img="https://mintcdn.com/awaitio/MjbT5AW66vMXbVck/images/components/select-single.png?fit=max&auto=format&n=MjbT5AW66vMXbVck&q=85&s=704233202ce9bdbebfa32bed8d3336e2" width="1200" height="750" data-path="images/components/select-single.png">
    `io.select.single()` asks to pick one option from a drop-down and returns the chosen option.
  </Card>

  <Card title="Select Multiple" href="/io/select-multiple" img="https://mintcdn.com/awaitio/MjbT5AW66vMXbVck/images/components/select-multiple.png?fit=max&auto=format&n=MjbT5AW66vMXbVck&q=85&s=a3671fa7e6387e89a002ee575a43b8f7" width="1200" height="750" data-path="images/components/select-multiple.png">
    `io.select.multiple()` asks to pick several options, listed under a drop-down, and returns the chosen options.
  </Card>

  <Card title="Select Table" href="/io/select-table" img="https://mintcdn.com/awaitio/MjbT5AW66vMXbVck/images/components/select-table.png?fit=max&auto=format&n=MjbT5AW66vMXbVck&q=85&s=e005b531046f68633dc0787febbbbd32" width="1200" height="750" data-path="images/components/select-table.png">
    `io.select.table()` asks to pick rows of a table and returns them.
  </Card>

  <Card title="Confirm" href="/io/confirm" img="https://mintcdn.com/awaitio/MjbT5AW66vMXbVck/images/components/confirm.png?fit=max&auto=format&n=MjbT5AW66vMXbVck&q=85&s=b95c4bd97000892dc2c2233fbaa50b83" width="1200" height="750" data-path="images/components/confirm.png">
    `io.confirm()` asks before something irreversible and returns `true` for Confirm or `false` for Cancel.
  </Card>
</Columns>

## Optional fields

Every input is required unless you chain `.optional()` to it. The field is marked *optional* next to its label, can be left empty, and the handler gets `undefined` for it; the component's own checks, such as `minLength`, run only when something was filled in:

```ts theme={null}
const { company, note } = await io.group({
  company: io.input.text('Company name'),
  note: io.input.textarea('Note').optional(), // string | undefined
});
```

Pass a condition to decide at run time: `.optional(!user.isAdmin)`. Text with only spaces and nothing picked in a multiple select count as empty.

## Custom validation

Chain `.validate()` to any input for a check of your own on top of the component's. The function gets the value the handler would get and returns a message, or nothing when the value is fine. The message shows under the field and the person answers again; the handler continues only once it returns nothing:

```ts theme={null}
const code = await io.input.text('Discount code').validate(async (code) => {
  const existing = await prisma.discount.findUnique({ where: { code } });

  return existing ? `${code} is already taken.` : undefined;
});
```

* It runs on the server, so it can be async and use your database; the browser cannot skip it.
* It runs only after the component's own checks pass, so `code` above is never empty. An optional field left empty and a disabled field skip it.
* A function that throws fails the run, like an error thrown in the handler.
* For a check across several fields, chain `.validate()` to [`io.group`](/io/group#custom-validation).

## Disabled fields

Every input takes `disabled: true` next to its own props. The field shows its starting value, usually `defaultValue`, and cannot be changed, and the server hands the handler that value whatever the browser sends:

```ts theme={null}
const { plan, note } = await io.group({
  plan: io.input.text('Plan', { defaultValue: account.plan, disabled: true }),
  note: io.input.textarea('Note'),
});
```

## The key is the name

The path of keys to a component becomes its method on `io`: `input: { text: inputText }` gives `io.input.text()`. Nothing in the component decides its name, so the same component can sit under any key, and the tree can be as deep or as flat as you like:

```ts theme={null}
components: {
  text: inputText, // io.text()
  forms: { long: inputTextarea }, // io.forms.long()
}
```

The same path, joined with dots (`input.text`), is the component's type: the server and the UI use it to tell components apart.

<Info>`group` and `layout` are reserved for `io.group()` and the layouts of pages. Neither can be a top-level key; the app refuses to start with them.</Info>

## Typed `io`

Actions get `io` typed from this tree. Create actions with `app.defineAction()` on the same instance, and every method, its props and what it returns are checked:

```ts actions/accounts/createAccountAction.ts theme={null}
import { app } from '../../app';

export const createAccountAction = app.defineAction({
  name: 'Create account',
  handler: async ({ io }) => {
    const company = await io.input.text('Company name', { placeholder: 'Acme Inc.' }); // string

    return `${company} has an account.`;
  },
});
```

A method of a component that is not registered does not exist on `io`, and a wrong prop is a type error. The server checks the props and every answer again, so nothing from the browser gets past them.

<Tip>Keep `new AwaitIO()` in its own file (`app.ts`) and import the instance in action files. Creating it in the file that calls `listen()` would make every action import that file back.</Tip>

## Write your own

A component is one `.tsx` file: zod schemas for its props and its answer, a check the server runs, and the React control. For example, a star rating:

```tsx components/rating/src/rating.tsx theme={null}
import { defineComponent } from 'awaitio';
import { z } from 'zod';

export const rating = defineComponent({
  props: z.object({ max: z.number().int().min(2).max(10).default(5) }),
  returns: z.number().int(),
  initialValue: () => {
    return 0;
  },
  // Runs on the server, so the browser cannot skip it.
  validate: ({ label, value }) => {
    return value < 1 ? `${label}: pick at least one star.` : null;
  },
  component: ({ props, value, onChange }) => {
    return (
      <div role="radiogroup">
        {Array.from({ length: props.max }, (_, index) => {
          return (
            <button key={index} type="button" onClick={() => onChange(index + 1)}>
              {index < value ? '★' : '☆'}
            </button>
          );
        })}
      </div>
    );
  },
});
```

`defineComponent()` takes:

| Option | Type | Required | Description |
| - | - | - | - |
| `props` | zod schema | Required | The props after the label, checked on the server. |
| `returns` | zod schema | Required | The answer. May transform it, e.g. into a `DateTime`. |
| `component` | React component | Required | The control. Gets the props below. |
| `validate` | function | Optional | Server check of the answer: a message or `null`. |
| `initialValue` | function | Optional | The starting value. Runs in the browser too. |
| `wrap` | `boolean` | Optional | `false` leaves out the label, the help text and the error: you draw the whole field. |
| `submitButton` | `boolean` | Optional | `false` leaves out the Continue button: the component sends the step with `submit`, like `io.confirm`. |

`component` gets:

| Prop | Type | Description |
| - | - | - |
| `label` | `string` | The first argument of the io call. |
| `props` | object | The parsed props, with defaults filled in. |
| `value` | answer | The current answer. |
| `onChange` | `(value) => void` | Call it with the new answer. |
| `submit` | `(value) => void` | Sets the answer and sends the step, for a component with `submitButton: false`. |
| `disabled` | `boolean` | `true` while the answer is being sent. |
| `autoFocus` | `boolean` | `true` for the first field of a step. |
| `error` | `string \| null` | What the server found wrong. |

`validate` runs only on the server, so the browser cannot skip it. `component` draws only the control; AwaitIO adds the label, the help text and the error around it, unless the component is [unwrapped](#unwrapped-components).

The browser half is built into the package with the Vite config from `awaitio/vite`:

```ts components/rating/vite.config.mts theme={null}
import { componentBuildConfig } from 'awaitio/vite';
import { defineConfig } from 'vite';

export default defineConfig(componentBuildConfig({ root: import.meta.url, entry: 'src/rating.tsx', exportName: 'rating' }));
```

Register it like any other component, and its key is its name:

```ts app.ts {4} theme={null}
components: {
  input: {
    text: inputText,
    rating, // io.input.rating('How did the call go?', { max: 5 }) returns a number
  },
}
```

Register your component under a key that already exists, `input: { text: myText }`, and it replaces the built-in one in every action, as long as its props and its answer fit.

## Building blocks

`awaitio/ui` has the pieces the built-in components are made of. They have no look of their own: each one draws the block of the theme your app was created with, the `theme` in `new AwaitIO()`. A component built from them looks like the rest of the app, changes with the theme and needs no CSS.

| Block | Draws |
| - | - |
| `Input` | A one-line text field. `value` and `onChange` work with a `string`. |
| `Textarea` | A text field over several lines. |
| `Select` | A drop-down for one of many `choices`. |
| `MultiSelect` | A drop-down and the chosen options under it in one frame, each with a × that removes it. |
| `ChoiceList` | Choices as cards, `mode: 'single'` or `'multiple'`. |
| `Checkbox` | A checkbox with its text to the right, for a yes or no answer. |
| `ButtonRow` | Buttons side by side, e.g. Confirm and Cancel. |
| `Button` | A button, `variant: 'primary'` or `'secondary'`. |
| `Field` | Label, control, help text and error of one field, in an [unwrapped](#unwrapped-components) component. |
| `Label` | The label of a field, in an unwrapped component. |
| `Hint` | Small text under a control. |

## Unwrapped components

By default AwaitIO puts your control into a `Field`: the label above it, the help text and the server's error under it. With `wrap: false` the component is unwrapped: none of that is added, and it lays out the whole field itself from the `label` and `error` it gets. Use it when the label or the error belong somewhere else, or when the field needs more than one control.

This is the rating unwrapped, drawn with `Field` and `Hint` from `awaitio/ui`, with the score under the stars:

```tsx components/rating/src/rating.tsx theme={null}
import { defineComponent } from 'awaitio';
import { Field, Hint } from 'awaitio/ui';
import { z } from 'zod';

export const rating = defineComponent({
  props: z.object({ max: z.number().int().min(2).max(10).default(5) }),
  returns: z.number().int(),
  initialValue: () => {
    return 0;
  },
  validate: ({ label, value }) => {
    return value < 1 ? `${label}: pick at least one star.` : null;
  },
  // The whole field is yours: label, the stars, a hint with the score and the error from the server.
  wrap: false,
  component: ({ label, props, value, onChange, error }) => {
    return (
      <Field label={label} error={error}>
        <div role="radiogroup">
          {Array.from({ length: props.max }, (_, index) => {
            return (
              <button key={index} type="button" onClick={() => onChange(index + 1)}>
                {index < value ? '★' : '☆'}
              </button>
            );
          })}
        </div>
        <Hint>
          {value} of {props.max} stars
        </Hint>
      </Field>
    );
  },
});
```


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.