Form
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
bun add @areia/formPeer dependencies: areia, ilha, and @areia/slots.
Import
import {
Form,
FloatingForm,
createFormState,
renderForm,
} from "@areia/form";Usage
Form is a factory: call it with your schema, default values, and options. It 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(),
age: z.number().min(0).max(120),
theme: z.enum(["light", "dark", "system"]),
active: z.boolean()
});
const MyForm = Form(
schema,
{ name: "John", age: 30, theme: "system", active: true },
{ onSubmit: (values) => console.log(values) }
);
export default ilha.render(() => <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(),
active: z.boolean(),
});
export const SettingsForm = Form(
schema,
{ name: "", active: true },
{
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.
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, defaultValues, options?)
| Parameter | Type | Description |
|---|---|---|
schema |
StandardSchemaV1 |
The validation schema (Zod, Valibot, ArkType, etc.). |
defaultValues |
Record<string, unknown> |
Initial field values. |
options |
FormOptions |
Optional configuration. |
Returns an ilha island. Call .define(tagName) to expose it as a custom element.
FormOptions
| Option | Type | Default | Description |
|---|---|---|---|
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, defaultValues, options?)
| Parameter | Type | Description |
|---|---|---|
schema |
StandardSchemaV1 |
The validation schema. |
defaultValues |
Record<string, unknown> |
Initial values. |
options |
FloatingFormOptions |
Optional configuration. |
Also returns an ilha island — use .define() the same way as Form.
FloatingFormOptions
| Option | Type | Default | Description |
|---|---|---|---|
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. |