Form
Schema-driven, auto-generating form components and state management.
Schema-driven, auto-generating form components for Areia.
Works with any Standard Schema library (Zod, Valibot, ArkType, etc.). Built on ilha signals and Areia field controls.
Installation
npm install @areia/formpnpm add @areia/formyarn add @areia/formbun add @areia/formPeer dependencies: areia, ilha, and @areia/slots.
Import
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.
import { ilha } from "ilha";
import { z } from "zod";
import { Form } from "@areia/form";
const schema = z.object({
name: z.string().default("John"),
age: z.number().min(0).max(120).default(30),
theme: z.enum(["light", "dark", "system"]).default("system"),
active: z.boolean().default(true),
});
const MyForm = Form(schema, {
onSubmit: (values) => console.log(values),
});
export default ilha(() => <MyForm />);Use with any framework via .define()
Because Form returns a normal ilha island, you can register it as a custom element with .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:
// 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
<script type="module" src="./settings-form.ts"></script>
<areia-settings-form></areia-settings-form>
React
import "./settings-form";
export function Page() {
return <areia-settings-form />;
}
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 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.
import { ilha } from "ilha";
import { z } from "zod";
import { Form } from "@areia/form";
const ColorForm = Form(
z.object({ brandColor: z.string() }),
{ brandColor: "#ff0000" },
{
uiOverrides: { brandColor: { type: "color", label: "Brand Color" } },
onSubmit: (values) => console.log(values),
}
);
export default ilha.render(() => <ColorForm />);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.
Settings
import { ilha } from "ilha";
import { z } from "zod";
import { FloatingForm } from "@areia/form";
const SceneControls = FloatingForm(
z.object({
speed: z.number().min(0.1).max(10),
wireframe: z.boolean(),
color: z.string(),
}),
{ speed: 1.0, wireframe: false, color: "#ff00ff" },
{
title: "Scene Controls",
uiOverrides: { color: { type: "color" } },
onChange: (values) => console.log("changed:", values),
}
);
export default ilha.render(() => (
<div class="relative w-full h-[400px]">
<SceneControls />
</div>
));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.
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:
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) 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. |