---
title: Form
description: Schema-driven, auto-generating form components and state management.
sidebar:
  order: 15
---

import { Demo1, Demo2, Demo3 } from "./form.demos";

Schema-driven, auto-generating form components for Areia.

Works with any [Standard Schema](https://standardschema.dev) library (Zod, Valibot, ArkType, etc.). Built on ilha signals and Areia field controls.

## Installation

```package-install
npm install @areia/form
```

Peer dependencies: `areia`, `ilha`, and `@areia/slots`.

## Import

```ts
import {
  Form,
  FloatingForm,
  createFormState,
  renderForm,
} from "@areia/form";
```

## Usage

Give every field a schema default (zod `.default()`) and call `Form` with just the schema — initial values come straight from the schema. Pass `{ defaultValues }` in options when you need explicit ones. The factory returns an ilha island with the schema captured in its closure, so the schema is never serialized through `data-ilha-props`.

<Preview
  code={
    'import { ilha } from "ilha";\nimport { z } from "zod";\nimport { Form } from "@areia/form";\n\nconst schema = z.object({\n  name: z.string().default("John"),\n  age: z.number().min(0).max(120).default(30),\n  theme: z.enum(["light", "dark", "system"]).default("system"),\n  active: z.boolean().default(true),\n});\n\nconst MyForm = Form(schema, {\n  onSubmit: (values) => console.log(values),\n});\n\nexport default ilha(() => <MyForm />);'
  }
  lang="tsx"
>
  <Demo1 client:load />
</Preview>

## Use with any framework via `.define()`

Because `Form` returns a normal ilha island, you can register it as a [custom element](https://ilha.build/guide/island/define/) with [`.define()`](https://ilha.build/guide/island/define/). That mounts and unmounts itself via `connectedCallback` / `disconnectedCallback`, so it works from React, Vue, Svelte, plain HTML, or a CMS template — without calling `ilha.mount()`.

Schema and callbacks stay in the factory closure (they cannot round-trip through HTML attributes). Register once at module scope, then drop the tag anywhere:

```ts
// settings-form.ts
import { Form } from "@areia/form";
import { z } from "zod";

const schema = z.object({
  name: z.string().default(""),
  active: z.boolean().default(true),
});

export const SettingsForm = Form(schema, {
  onSubmit: (values) => console.log(values),
  onChange: (values) => console.log("changed:", values),
});

// Safe during SSR — no-ops where customElements is unavailable.
SettingsForm.define("areia-settings-form");
```

### Plain HTML

```html
<script type="module" src="./settings-form.ts"></script>
<areia-settings-form></areia-settings-form>
```

### React

```tsx
import "./settings-form";

export function Page() {
  return <areia-settings-form />;
}
```

### Vue

```vue
<script setup>
import "./settings-form";
</script>

<template>
  <areia-settings-form />
</template>
```

`FloatingForm` works the same way — call `.define("areia-scene-controls")` on the island returned by the factory.

For rich runtime updates (title, width, etc.) after mount, set the host's [`props`](https://ilha.build/guide/island/define/) property. Prefer capturing submit/change handlers in the factory options so they stay typed and outside the attribute surface.

## Examples

### UI Overrides

Manually override field types, labels, or extra props with `uiOverrides`.

<Preview
  code={
    'import { ilha } from "ilha";\nimport { z } from "zod";\nimport { Form } from "@areia/form";\n\nconst ColorForm = Form(\n  z.object({ brandColor: z.string() }),\n  { brandColor: "#ff0000" },\n  {\n    uiOverrides: { brandColor: { type: "color", label: "Brand Color" } },\n    onSubmit: (values) => console.log(values),\n  }\n);\n\nexport default ilha.render(() => <ColorForm />);'
  }
  lang="tsx"
>
  <Demo2 client:load />
</Preview>

### Floating Form (DevTools)

`FloatingForm` is also a factory. It creates a draggable, collapsible overlay panel — useful for generative art, 3D scenes, or internal control panels.

<Preview
  code={
    'import { ilha } from "ilha";\nimport { z } from "zod";\nimport { FloatingForm } from "@areia/form";\n\nconst SceneControls = FloatingForm(\n  z.object({\n    speed: z.number().min(0.1).max(10),\n    wireframe: z.boolean(),\n    color: z.string(),\n  }),\n  { speed: 1.0, wireframe: false, color: "#ff00ff" },\n  {\n    title: "Scene Controls",\n    uiOverrides: { color: { type: "color" } },\n    onChange: (values) => console.log("changed:", values),\n  }\n);\n\nexport default ilha.render(() => (\n  <div class="relative w-full h-[400px]">\n    <SceneControls />\n  </div>\n));'
  }
  lang="tsx"
>
  <Demo3 client:load />
</Preview>

### Headless State Manager

If you only need reactive state for your own UI, use `createFormState`. Values and `field(path)` accessors are ilha signals — usable with `bind:value` / `bind:checked`, or observed from any framework via `effect`.

```ts
import { z } from "zod";
import { effect } from "ilha";
import { createFormState } from "@areia/form";

const schema = z.object({
  username: z.string().min(3),
});

const state = createFormState(schema, { username: "" });

effect(() => {
  console.log("Current values:", state.values());
  console.log("Validation errors:", state.errors());
  console.log("Dirty:", state.isDirty());
});

// Programmatic updates
state.setValue("username", "admin");

// Or bind a field accessor directly in ilha JSX:
// <Input bind:value={state.field("username")} />

await state.validate();
```

### Static render (no island)

`renderForm` produces markup without wiring submit/validation. Pass an existing `state` when you own the store yourself:

```tsx
import { renderForm, createFormState } from "@areia/form";

const state = createFormState(schema, defaults);

renderForm({
  schema,
  defaultValues: defaults,
  state,
  submitLabel: "Save",
});
```

## API Reference

### Form(schema, options?)

| Parameter | Type               | Description                                          |
| --------- | ------------------ | ---------------------------------------------------- |
| `schema`  | `StandardSchemaV1` | The validation schema (Zod, Valibot, ArkType, etc.). |
| `options` | `FormOptions`      | Optional configuration.                              |

Returns an ilha island. Call [`.define(tagName)`](https://ilha.build/guide/island/define/) to expose it as a custom element.

**FormOptions**

| Option          | Type                                | Default     | Description                                              |
| --------------- | ----------------------------------- | ----------- | -------------------------------------------------------- |
| `defaultValues` | `Record<string, unknown>`           | from schema | Initial values. Omit to derive from schema `.default()`. |
| `uiOverrides`   | `UIOverrides`                       | -           | Manual overrides for field type, labels, options.        |
| `onSubmit`      | `(values: T, event: Event) => void` | -           | Called when the form passes validation and submits.      |
| `onChange`      | `(values: T) => void`               | -           | Fired when values diverge from the defaults.             |
| `submitLabel`   | `string`                            | `"Submit"`  | Text rendered on the submit button.                      |
| `validateOn`    | `"change" \| "blur" \| "submit"`    | -           | When validation rules are checked.                       |

### FloatingForm(schema, options?)

| Parameter | Type                  | Description             |
| --------- | --------------------- | ----------------------- |
| `schema`  | `StandardSchemaV1`    | The validation schema.  |
| `options` | `FloatingFormOptions` | Optional configuration. |

Also returns an ilha island — use `.define()` the same way as `Form`.

**FloatingFormOptions**

| Option          | Type                       | Default            | Description                                              |
| --------------- | -------------------------- | ------------------ | -------------------------------------------------------- |
| `defaultValues` | `Record<string, unknown>`  | from schema        | Initial values. Omit to derive from schema `.default()`. |
| `uiOverrides`   | `UIOverrides`              | -                  | Overrides.                                               |
| `title`         | `string`                   | `"Controls"`       | Title rendered in the drag handle.                       |
| `collapsed`     | `boolean`                  | `false`            | Whether the panel starts collapsed.                      |
| `position`      | `{ x: number; y: number }` | `{ x: 20, y: 20 }` | Initial position.                                        |
| `width`         | `string \| number`         | `"320px"`          | Fixed width of the floating panel.                       |
| `onClose`       | `() => void`               | -                  | Shows a close button when set.                           |
| `onChange`      | `(values: T) => void`      | -                  | Called when values become dirty.                         |
| `onSubmit`      | `(values: T) => void`      | -                  | Called on form submit.                                   |

### createFormState(schema, defaultValues?)

| Return     | Type                                     | Description                                              |
| ---------- | ---------------------------------------- | -------------------------------------------------------- |
| `values`   | `() => T`                                | Current form values (ilha signal).                       |
| `isDirty`  | `() => boolean`                          | `true` when any top-level key differs from the defaults. |
| `isValid`  | `() => boolean`                          | `true` when there are no validation errors.              |
| `errors`   | `() => Record<string, string>`           | Path → message map.                                      |
| `setValue` | `(path: string, value: unknown) => void` | Write a field by dotted path.                            |
| `field`    | `(path: string) => SignalAccessor`       | Bindable slice for `bind:value` / `bind:checked`.        |
| `validate` | `() => Promise<boolean>`                 | Run Standard Schema validation.                          |
| `reset`    | `() => void`                             | Restore defaults and clear errors.                       |
