Skip to main content
io offers exactly the components you register. Each one is its own package, named after its method: io.select.table() comes from @awaitio/select-table. Install the ones you use, or write your own, and register them in components when you create the app:
app.ts

Available components

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

Input Text

io.input.text() asks for one line of text and returns a string.

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:
The same path, joined with dots (input.text), is the component’s type: the server and the UI use it to tell components apart.
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.

Typed io

Actions get io typed from this tree. Create actions with app.action() on the same instance, and every method, its props and what it returns are checked:
actions/accounts/createAccount.ts
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.
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.

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. This is the star rating from the demo app:
components/rating/src/rating.tsx
defineComponent() takes: component gets: 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. The browser half is built into the package with the Vite config from awaitio/vite:
components/rating/vite.config.mts
Register it like any other component, and its key is its name:
app.ts
A star rating with four of five stars selected, above a text area
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.

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:
components/rating/src/rating.tsx