Skip to content

Form

Schema-driven, auto-generating form components and state management.

Updated View as Markdown

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/form

Peer 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.

Scene Controls
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.
Navigation

Type to search…

↑↓ navigate↵ selectEsc close