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
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 onio: 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:
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
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.
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
app.ts

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 aField: 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