TanStack Form
Integrate Ark Preset components with TanStack Form for powerful, type-safe form state management in Solid.js.
DocsTanStack Form provides a headless form library with first-class Solid.js support. Combined with Ark Preset’s accessible form components, you get a complete form solution with proper styling, validation, and accessibility.
import { createForm } from "@tanstack/solid-form";
import { createListCollection } from "@ark-ui/solid";
import { Index, Show } from "solid-js";
import { parseDate } from "@internationalized/date";
import { z } from "zod";
import {
Input,
Button,
Textarea,
NumberInput,
Select,
SelectLabel,
SelectTrigger,
SelectContent,
SelectItem,
Checkbox,
CheckboxLabel,
Switch,
SwitchLabel,
Slider,
SliderLabel,
SliderControl,
SliderThumb,
RatingGroup,
RatingGroupLabel,
Combobox,
ComboboxLabel,
ComboboxInputTrigger,
ComboboxContent,
ComboboxItem,
TagsInput,
RadioGroup,
RadioGroupItem,
SegmentGroup,
SegmentGroupItem,
Toggle,
ToggleGroup,
ToggleGroupItem,
PasswordInput,
DatePicker,
} from "~/components/tanstack-form";
import { labelVariants } from "@ark-preset/core";
// ── Zod schema ──
const formSchema = z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
email: z.string().min(1, "Email is required").email("Invalid email format"),
age: z.number().min(13, "Must be at least 13 years old"),
role: z.string().min(1, "Please select a role"),
country: z.string(),
bio: z.string().max(200, "Bio must be under 200 characters"),
plan: z.string(),
rating: z.number(),
priority: z.string(),
volume: z.number(),
notifications: z.boolean(),
skills: z.array(z.string()).max(10, "Maximum 10 skills allowed"),
password: z
.string()
.refine((v) => v.length === 0 || v.length >= 6, "Password must be at least 6 characters"),
accepted: z.boolean().refine((v) => v, { message: "You must accept the terms" }),
startDate: z.string().min(1, "Start date is required"),
bold: z.boolean(),
alignment: z.string(),
});
// ── Field helpers ──
function fieldError(field: any): string | undefined {
const err = field.state.meta.errors[0];
if (!err) return undefined;
if (typeof err === "string") return err;
return (err as { message?: string }).message;
}
function FieldHint(props: { field: any }) {
return (
<Show when={fieldError(props.field)}>
{(msg) => <div class="text-destructive text-sm">{msg()}</div>}
</Show>
);
}
// ── Data ──
const countries = createListCollection({
items: [
{ label: "Indonesia", value: "id" },
{ label: "Malaysia", value: "my" },
{ label: "Singapore", value: "sg" },
{ label: "Thailand", value: "th" },
{ label: "Vietnam", value: "vn" },
{ label: "Japan", value: "jp" },
{ label: "South Korea", value: "kr" },
{ label: "United States", value: "us" },
{ label: "United Kingdom", value: "gb" },
{ label: "Australia", value: "au" },
],
});
const roles = createListCollection({
items: [
{ label: "Developer", value: "developer" },
{ label: "Designer", value: "designer" },
{ label: "Product Manager", value: "pm" },
{ label: "QA Engineer", value: "qa" },
{ label: "Data Analyst", value: "analyst" },
{ label: "DevOps Engineer", value: "devops" },
{ label: "Tech Lead", value: "tech-lead" },
{ label: "Engineering Manager", value: "eng-manager" },
{ label: "CTO", value: "cto" },
{ label: "VP of Engineering", value: "vp-eng" },
],
});
// ── Component ──
export default function TanstackFormDemo() {
const form = createForm(() => ({
defaultValues: {
name: "",
email: "",
age: 0,
role: "",
country: "",
bio: "",
plan: "free",
rating: 0,
priority: "medium",
volume: 50,
notifications: false,
skills: [] as string[],
password: "",
accepted: false,
startDate: "2026-06-01",
bold: false,
alignment: "left",
},
validators: {
onBlur: formSchema,
onSubmit: (values: unknown) => {
console.log("[dbg] onSubmit validator called");
const r = formSchema.safeParse(values);
console.log("[dbg] onSubmit validator success =", r.success);
return r.success ? undefined : r.error;
},
},
onSubmit: async ({ value }) => {
console.log("[dbg] form onSubmit called, accepted =", value.accepted);
alert(JSON.stringify(value, null, 2));
},
}));
return (
<form
onSubmit={(e) => {
console.log("[dbg] native submit event");
e.preventDefault();
e.stopPropagation();
try {
const r = form.handleSubmit() as any;
if (r && typeof r.catch === "function") {
r.catch((err: unknown) => console.log("[dbg] handleSubmit rejected", err));
}
} catch (err) {
console.log("[dbg] handleSubmit threw", err);
}
}}
class="flex flex-col gap-5"
>
{/* ── Name ── */}
<form.Field
name="name"
children={(field) => (
<Input
name={field().name}
value={field().state.value}
label="Name"
placeholder="Enter your name"
error={fieldError(field())}
onBlur={field().handleBlur}
onInput={(e) => field().handleChange(e.currentTarget.value)}
/>
)}
/>
{/* ── Email ── */}
<form.Field
name="email"
children={(field) => (
<Input
name={field().name}
value={field().state.value}
label="Email"
placeholder="email@example.com"
error={fieldError(field())}
onBlur={field().handleBlur}
onInput={(e) => field().handleChange(e.currentTarget.value)}
/>
)}
/>
{/* ── Age ── */}
<form.Field
name="age"
children={(field) => (
<NumberInput
name={field().name}
label="Age"
value={String(field().state.value)}
error={!!fieldError(field())}
min={0}
max={150}
onValueChange={(e) => field().handleChange(e.valueAsNumber)}
onBlur={field().handleBlur}
/>
)}
/>
{/* ── Password ── */}
<form.Field
name="password"
children={(field) => (
<PasswordInput
name={field().name}
value={field().state.value}
label="Password"
placeholder="Enter password"
error={!!fieldError(field())}
onInput={(e: InputEvent) => field().handleChange((e.target as HTMLInputElement).value)}
onBlur={field().handleBlur}
/>
)}
/>
{/* ── Role ── */}
<form.Field
name="role"
children={(field) => (
<div class="not-prose flex flex-col gap-1">
<Select
name={field().name}
collection={roles}
value={field().state.value ? [field().state.value] : []}
error={!!fieldError(field())}
onValueChange={(e) => {
field().handleChange(e.value[0]);
field().handleBlur();
}}
onOpenChange={(details) => {
if (!details.open) field().handleBlur();
}}
>
<SelectLabel>Role</SelectLabel>
<SelectTrigger placeholder="Select a role" />
<SelectContent>
<Index each={roles.items}>
{(item) => <SelectItem item={item()}>{item().label}</SelectItem>}
</Index>
</SelectContent>
</Select>
<FieldHint field={field()} />
</div>
)}
/>
{/* ── Country (Combobox) ── */}
<form.Field
name="country"
children={(field) => (
<div class="not-prose flex flex-col gap-1">
<Combobox
name={field().name}
collection={countries}
value={field().state.value ? [field().state.value] : []}
onValueChange={(e) => field().handleChange(e.value[0])}
onOpenChange={(details) => {
if (!details.open) field().handleBlur();
}}
>
<ComboboxLabel>Country</ComboboxLabel>
<ComboboxInputTrigger placeholder="Search country..." />
<ComboboxContent>
<Index each={countries.items}>
{(item) => <ComboboxItem item={item()}>{item().label}</ComboboxItem>}
</Index>
</ComboboxContent>
</Combobox>
</div>
)}
/>
{/* ── Plan (RadioGroup) ── */}
<form.Field
name="plan"
children={(field) => (
<fieldset>
<div class="not-prose flex flex-col gap-2">
<legend class={labelVariants()}>Plan</legend>
<RadioGroup
name={field().name}
value={field().state.value}
onValueChange={(e) => field().handleChange(e.value ?? "")}
onBlur={field().handleBlur}
>
<RadioGroupItem value="free">Free</RadioGroupItem>
<RadioGroupItem value="pro">Pro</RadioGroupItem>
<RadioGroupItem value="enterprise">Enterprise</RadioGroupItem>
</RadioGroup>
</div>
</fieldset>
)}
/>
{/* ── Priority (SegmentGroup) ── */}
<form.Field
name="priority"
children={(field) => (
<fieldset>
<div class="not-prose flex flex-col gap-2">
<legend class={labelVariants()}>Priority</legend>
<SegmentGroup
name={field().name}
value={field().state.value}
onValueChange={(e) => field().handleChange(e.value ?? "")}
onBlur={field().handleBlur}
>
<SegmentGroupItem value="low">Low</SegmentGroupItem>
<SegmentGroupItem value="medium">Medium</SegmentGroupItem>
<SegmentGroupItem value="high">High</SegmentGroupItem>
</SegmentGroup>
</div>
</fieldset>
)}
/>
{/* ── Rating ── */}
<form.Field
name="rating"
children={(field) => (
<RatingGroup
name={field().name}
value={field().state.value}
count={5}
orientation="vertical"
onValueChange={(e) => field().handleChange(e.value)}
onBlur={field().handleBlur}
>
<RatingGroupLabel>Rating</RatingGroupLabel>
</RatingGroup>
)}
/>
{/* ── Volume (Slider) ── */}
<form.Field
name="volume"
children={(field) => (
<Slider
name={field().name}
value={[field().state.value]}
min={0}
max={100}
step={1}
onValueChange={(e) => field().handleChange(e.value[0])}
onBlur={field().handleBlur}
>
<SliderLabel>Volume: {field().state.value}%</SliderLabel>
<SliderControl>
<SliderThumb index={0} />
</SliderControl>
</Slider>
)}
/>
{/* ── Start Date (DatePicker) ── */}
<form.Field
name="startDate"
children={(field) => (
<DatePicker
name={field().name}
label="Start Date"
placeholder="Pick a date"
value={field().state.value ? [parseDate(field().state.value)] : []}
error={!!fieldError(field())}
onValueChange={(e) => field().handleChange(e.value[0] ? String(e.value[0]) : "")}
/>
)}
/>
{/* ── Bio ── */}
<form.Field
name="bio"
children={(field) => (
<Textarea
name={field().name}
value={field().state.value}
label="Bio"
placeholder="Tell us about yourself"
error={fieldError(field())}
onBlur={field().handleBlur}
onInput={(e) => field().handleChange(e.currentTarget.value)}
/>
)}
/>
{/* ── Skills (TagsInput) ── */}
<form.Field
name="skills"
children={(field) => (
<div class="not-prose flex flex-col gap-1">
<TagsInput
name={field().name}
value={field().state.value}
label="Skills"
placeholder="Type skill and press Enter"
onValueChange={(e) => field().handleChange(e.value)}
/>
</div>
)}
/>
{/* ── Notifications (Switch) ── */}
<form.Field
name="notifications"
children={(field) => (
<Switch
name={field().name}
checked={field().state.value}
onCheckedChange={(e) => field().handleChange(!!e.checked)}
onBlur={field().handleBlur}
>
<SwitchLabel>Enable email notifications</SwitchLabel>
</Switch>
)}
/>
{/* ── Bold (Toggle) ── */}
<form.Field
name="bold"
children={(field) => (
<Toggle
name={field().name}
pressed={field().state.value}
onPressedChange={(pressed) => field().handleChange(!!pressed)}
onBlur={field().handleBlur}
>
Bold
</Toggle>
)}
/>
{/* ── Alignment (ToggleGroup) ── */}
<form.Field
name="alignment"
children={(field) => (
<ToggleGroup
value={[field().state.value]}
onValueChange={(e) => field().handleChange(e.value[0])}
onBlur={field().handleBlur}
>
<ToggleGroupItem value="left">Left</ToggleGroupItem>
<ToggleGroupItem value="center">Center</ToggleGroupItem>
<ToggleGroupItem value="right">Right</ToggleGroupItem>
</ToggleGroup>
)}
/>
{/* ── Accepted (Checkbox) ── */}
<form.Field
name="accepted"
children={(field) => (
<div class="not-prose flex flex-col gap-1">
<Checkbox
name={field().name}
checked={field().state.value}
invalid={!!fieldError(field())}
onCheckedChange={(e) => {
field().handleChange(!!e.checked);
}}
onBlur={field().handleBlur}
>
<CheckboxLabel>I accept the terms and conditions</CheckboxLabel>
</Checkbox>
<FieldHint field={field()} />
</div>
)}
/>
{/* ── Submit ── */}
<form.Subscribe
selector={(state) => ({
isSubmitting: state.isSubmitting,
})}
children={(state) => (
<Button type="submit" disabled={state().isSubmitting}>
{state().isSubmitting ? "Submitting..." : "Submit"}
</Button>
)}
/>
{/* DEBUG: live form values (temporary) */}
<form.Subscribe
selector={(s) => s.values}
children={(v) => <pre class="text-xs opacity-50">{JSON.stringify(v())}</pre>}
/>
</form>
);
}Key Features
- Type-safe form state — Fully typed fields, values, and validation
- Schema validation — Works with Zod, Valibot, ArkType, and Effect
- Fine-grained reactivity — Solid.js signals for efficient re-renders
- Async validation — Debounced and async validators built-in
- Custom field components — Compose reusable field patterns with
createFormHook
Installation
Install TanStack Form:
npm install @tanstack/solid-formOptionally install a schema validation library:
npm install zod
# or
npm install valibot
# or
npm install arktypeBasic Usage
Create a form with createForm and use form.Field to wire up Ark Preset components:
import { createForm } from "@tanstack/solid-form";
import { Input } from "~/components/input";
import { Button } from "~/components/button";
function SignupForm() {
const form = createForm(() => ({
defaultValues: {
name: "",
email: "",
},
onSubmit: async ({ value }) => {
console.log("Submit", value);
},
}));
return (
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
class="flex flex-col gap-4 max-w-sm"
>
<form.Field
name="name"
children={(field) => (
<Input
name={field().name}
value={field().state.value}
label="Name"
placeholder="Enter your name"
onBlur={field().handleBlur}
onInput={(e) => field().handleChange(e.currentTarget.value)}
/>
)}
/>
<form.Field
name="email"
children={(field) => (
<Input
name={field().name}
value={field().state.value}
label="Email"
placeholder="Enter your email"
onBlur={field().handleBlur}
onInput={(e) => field().handleChange(e.currentTarget.value)}
/>
)}
/>
<Button type="submit">Submit</Button>
</form>
);
}Validation
Inline Validators
Pass synchronous or asynchronous validators directly to form.Field:
<form.Field
name="email"
validators={{
onChange: ({ value }) => {
if (!value) return "Email is required";
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value))
return "Invalid email format";
return undefined;
},
onChangeAsyncDebounceMs: 500,
onChangeAsync: async ({ value }) => {
// Simulate async check (e.g., email uniqueness)
await new Promise((r) => setTimeout(r, 300));
return value === "taken@example.com"
? "Email is already in use"
: undefined;
},
}}
children={(field) => (
<Input
name={field().name}
value={field().state.value}
label="Email"
placeholder="Enter your email"
error={
field().state.meta.isTouched && field().state.meta.errors.length > 0
? field().state.meta.errors.join(", ")
: undefined
}
onBlur={field().handleBlur}
onInput={(e) => field().handleChange(e.currentTarget.value)}
/>
)}
/>Schema Validation with Zod
Use Zod schemas for declarative validation:
import { z } from "zod";
import { createForm } from "@tanstack/solid-form";
const signupSchema = z.object({
name: z.string().min(3, "Name must be at least 3 characters"),
email: z.string().email("Invalid email address"),
age: z.coerce.number().min(18, "Must be at least 18"),
});
function SignupForm() {
const form = createForm(() => ({
defaultValues: {
name: "",
email: "",
age: 0,
},
validators: {
onChange: signupSchema,
},
onSubmit: async ({ value }) => {
console.log("Validated data:", value);
},
}));
return (
<form onSubmit={(e) => { e.preventDefault(); form.handleSubmit(); }}>
<form.Field
name="name"
children={(field) => (
<Input
name={field().name}
value={field().state.value}
label="Name"
error={
field().state.meta.isTouched && field().state.meta.errors[0]
? field().state.meta.errors[0]
: undefined
}
onBlur={field().handleBlur}
onInput={(e) => field().handleChange(e.currentTarget.value)}
/>
)}
/>
{/* ... more fields */}
</form>
);
}TanStack Form also supports Valibot, ArkType, and Effect schemas via Standard Schema — no adapter needed.
Subscribe to Form State
Use form.Subscribe to reactively track form-level state:
<form.Subscribe
selector={(state) => ({
canSubmit: state.canSubmit,
isSubmitting: state.isSubmitting,
})}
children={(state) => (
<Button type="submit" disabled={!state().canSubmit}>
{state().isSubmitting ? "Submitting..." : "Submit"}
</Button>
)}
/>Working with Non-Input Components
Select
import { createListCollection } from "@ark-ui/solid";
import { Index } from "solid-js";
import { Select, SelectLabel, SelectTrigger, SelectContent, SelectItem } from "~/components/select";
const frameworks = createListCollection({
items: [
{ label: "React", value: "react" },
{ label: "Solid", value: "solid" },
{ label: "Vue", value: "vue" },
],
});
// Inside your form:
<form.Field
name="framework"
validators={{
onBlur: ({ value }) => (!value ? "Please select a framework" : undefined),
}}
children={(field) => (
<>
<Select
name={field().name}
collection={frameworks}
value={field().state.value ? [field().state.value] : []}
onValueChange={(e) => field().handleChange(e.value[0])}
onOpenChange={(details) => {
if (!details.open) field().handleBlur();
}}
>
<SelectLabel>Framework</SelectLabel>
<SelectTrigger placeholder="Select a framework" />
<SelectContent>
<Index each={frameworks.items}>
{(item) => <SelectItem item={item()}>{item().label}</SelectItem>}
</Index>
</SelectContent>
</Select>
{field().state.meta.errors[0] && (
<div class="text-destructive text-sm mt-1">{field().state.meta.errors[0]}</div>
)}
</>
)}
/>Checkbox
import { Checkbox, CheckboxLabel } from "~/components/checkbox";
<form.Field
name="accepted"
validators={{
onChange: ({ value }) => !value ? "You must accept the terms" : undefined,
onBlur: ({ value }) => !value ? "You must accept the terms" : undefined,
}}
children={(field) => (
<>
<Checkbox
name={field().name}
checked={field().state.value}
invalid={!!field().state.meta.errors[0]}
onCheckedChange={(e) => field().handleChange(!!e.checked)}
onBlur={field().handleBlur}
>
<CheckboxLabel>I accept the terms and conditions</CheckboxLabel>
</Checkbox>
{field().state.meta.errors[0] && (
<div class="text-destructive text-sm mt-1">{field().state.meta.errors[0]}</div>
)}
</>
)}
/>Switch
import { Switch } from "~/components/switch";
<form.Field
name="notifications"
children={(field) => (
<Switch
name={field().name}
checked={field().state.value}
onCheckedChange={(e) => field().handleChange(!!e.checked)}
onBlur={field().handleBlur}
>
Enable notifications
</Switch>
)}
/>Textarea
import { Textarea } from "~/components/textarea";
<form.Field
name="bio"
validators={{
onChange: ({ value }) =>
value.length > 500 ? "Bio must be under 500 characters" : undefined,
}}
children={(field) => (
<Textarea
name={field().name}
value={field().state.value}
label="Bio"
placeholder="Tell us about yourself"
error={
field().state.meta.errors[0]
? field().state.meta.errors[0]
: undefined
}
onBlur={field().handleBlur}
onInput={(e) => field().handleChange(e.currentTarget.value)}
/>
)}
/>Radio Group
import { RadioGroup } from "~/components/radio-group";
<form.Field
name="plan"
validators={{
onChange: ({ value }) => (!value ? "Please select a plan" : undefined),
onBlur: ({ value }) => (!value ? "Please select a plan" : undefined),
}}
children={(field) => (
<RadioGroup
name={field().name}
value={field().state.value}
label="Subscription Plan"
error={field().state.meta.errors[0]}
onChange={(e) => field().handleChange(e.value)}
onBlur={field().handleBlur}
options={[
{ value: "free", label: "Free" },
{ value: "pro", label: "Pro" },
{ value: "enterprise", label: "Enterprise" },
]}
/>
)}
/>Custom Field Components
For reusable field patterns, use createFormHookContexts and createFormHook:
// src/hooks/form-context.tsx
import { createFormHookContexts } from "@tanstack/solid-form";
export const { fieldContext, formContext, useFieldContext } =
createFormHookContexts();// src/components/text-field.tsx
import { useFieldContext } from "../hooks/form-context";
import { Input } from "~/components/input";
export function TextField(props: { label: string; placeholder?: string }) {
const field = useFieldContext<string>();
return (
<Input
name={field().name}
value={field().state.value}
label={props.label}
placeholder={props.placeholder}
error={
field().state.meta.isTouched && field().state.meta.errors[0]
? field().state.meta.errors[0]
: undefined
}
onBlur={field().handleBlur}
onInput={(e) => field().handleChange(e.currentTarget.value)}
/>
);
}// src/hooks/form.ts
import { createFormHook } from "@tanstack/solid-form";
import { fieldContext, formContext } from "./form-context";
import { TextField } from "../components/text-field";
export const { useAppForm } = createFormHook({
fieldContext,
formContext,
fieldComponents: { TextField },
formComponents: {},
});// Usage
import { useAppForm } from "../hooks/form";
function ProfileForm() {
const form = useAppForm(() => ({
defaultValues: { firstName: "", lastName: "" },
onSubmit: async ({ value }) => console.log(value),
}));
return (
<form onSubmit={(e) => { e.preventDefault(); form.handleSubmit(); }}>
<form.AppField
name="firstName"
children={(field) => <field.TextField label="First Name" />}
/>
<form.AppField
name="lastName"
children={(field) => <field.TextField label="Last Name" />}
/>
</form>
);
}Full Example
Here’s a complete form with Zod schema validation — this is the same code used in the live demo at the top of this page:
import { createForm } from "@tanstack/solid-form";
import { createListCollection } from "@ark-ui/solid";
import { Index, Show } from "solid-js";
import { parseDate } from "@internationalized/date";
import { z } from "zod";
import {
Input, Button, Textarea, NumberInput,
Select, SelectLabel, SelectTrigger, SelectContent, SelectItem,
Checkbox, CheckboxLabel,
Switch, SwitchLabel,
Slider, SliderLabel, SliderControl, SliderThumb,
RatingGroup, RatingGroupLabel,
Combobox, ComboboxLabel, ComboboxInputTrigger, ComboboxContent, ComboboxItem,
TagsInput,
RadioGroup, RadioGroupItem,
SegmentGroup, SegmentGroupItem,
Toggle, ToggleIndicator,
ToggleGroup, ToggleGroupItem,
PasswordInput, DatePicker,
} from "@ark-preset/solid";
import { labelVariants } from "@ark-preset/core";
// ── Zod schema ──
const formSchema = z.object({
name: z.string().min(2, "Name must be at least 2 characters"),
email: z.string().min(1, "Email is required").email("Invalid email format"),
age: z.number().min(13, "Must be at least 13 years old"),
role: z.string().min(1, "Please select a role"),
country: z.string(),
bio: z.string().max(200, "Bio must be under 200 characters"),
plan: z.string(),
rating: z.number(),
priority: z.string(),
volume: z.number(),
notifications: z.boolean(),
skills: z.array(z.string()).max(10, "Maximum 10 skills allowed"),
password: z
.string()
.refine(
(v) => v.length === 0 || v.length >= 6,
"Password must be at least 6 characters",
),
accepted: z.boolean().refine((v) => v, { message: "You must accept the terms" }),
startDate: z.string().min(1, "Start date is required"),
bold: z.boolean(),
alignment: z.string(),
});
// ── Field helpers ──
function fieldError(field: any): string | undefined {
const err = field.state.meta.errors[0];
if (!err) return undefined;
if (typeof err === "string") return err;
return (err as { message?: string }).message;
}
function FieldHint(props: { field: any }) {
return (
<Show when={fieldError(props.field)}>
{(msg) => <div class="text-destructive text-sm">{msg()}</div>}
</Show>
);
}
// ── Data ──
const countries = createListCollection({
items: [
{ label: "Indonesia", value: "id" },
{ label: "Malaysia", value: "my" },
{ label: "Singapore", value: "sg" },
{ label: "Thailand", value: "th" },
{ label: "Vietnam", value: "vn" },
{ label: "Japan", value: "jp" },
{ label: "South Korea", value: "kr" },
{ label: "United States", value: "us" },
{ label: "United Kingdom", value: "gb" },
{ label: "Australia", value: "au" },
],
});
const roles = createListCollection({
items: [
{ label: "Developer", value: "developer" },
{ label: "Designer", value: "designer" },
{ label: "Product Manager", value: "pm" },
{ label: "QA Engineer", value: "qa" },
{ label: "Data Analyst", value: "analyst" },
{ label: "DevOps Engineer", value: "devops" },
{ label: "Tech Lead", value: "tech-lead" },
{ label: "Engineering Manager", value: "eng-manager" },
{ label: "CTO", value: "cto" },
{ label: "VP of Engineering", value: "vp-eng" },
],
});
// ── Component ──
export default function TanstackFormDemo() {
const form = createForm(() => ({
defaultValues: {
name: "", email: "", age: 0, role: "", country: "",
bio: "", plan: "free", rating: 0, priority: "medium",
volume: 50, notifications: false, skills: [] as string[],
password: "", accepted: false, startDate: "2026-06-01",
bold: false, alignment: "left",
},
validators: {
onBlur: formSchema,
onSubmit: formSchema,
},
onSubmit: async ({ value }) => {
alert(JSON.stringify(value, null, 2));
},
}));
return (
<form
onSubmit={(e) => {
e.preventDefault();
e.stopPropagation();
form.handleSubmit();
}}
class="flex flex-col gap-5"
>
<form.Field name="name" children={(field) => (
<Input name={field().name} value={field().state.value} label="Name"
placeholder="Enter your name" error={fieldError(field())}
onBlur={field().handleBlur}
onInput={(e) => field().handleChange(e.currentTarget.value)} />
)} />
<form.Field name="email" children={(field) => (
<Input name={field().name} value={field().state.value} label="Email"
placeholder="email@example.com" error={fieldError(field())}
onBlur={field().handleBlur}
onInput={(e) => field().handleChange(e.currentTarget.value)} />
)} />
<form.Field name="age" children={(field) => (
<NumberInput name={field().name} label="Age"
value={String(field().state.value)} error={!!fieldError(field())}
min={0} max={150}
onValueChange={(e) => field().handleChange(e.valueAsNumber)}
onBlur={field().handleBlur} />
)} />
<form.Field name="password" children={(field) => (
<PasswordInput name={field().name} value={field().state.value}
label="Password" placeholder="Enter password"
error={!!fieldError(field())}
onInput={(e) => field().handleChange((e.target as HTMLInputElement).value)}
onBlur={field().handleBlur} />
)} />
<form.Field name="role" children={(field) => (
<div class="not-prose flex flex-col gap-1">
<Select name={field().name} collection={roles}
value={field().state.value ? [field().state.value] : []}
error={!!fieldError(field())}
onValueChange={(e) => { field().handleChange(e.value[0]); field().handleBlur(); }}
onOpenChange={(details) => { if (!details.open) field().handleBlur(); }}>
<SelectLabel>Role</SelectLabel>
<SelectTrigger placeholder="Select a role" />
<SelectContent>
<Index each={roles.items}>
{(item) => <SelectItem item={item()}>{item().label}</SelectItem>}
</Index>
</SelectContent>
</Select>
<FieldHint field={field()} />
</div>
)} />
<form.Field name="country" children={(field) => (
<div class="not-prose flex flex-col gap-1">
<Combobox name={field().name} collection={countries}
value={field().state.value ? [field().state.value] : []}
onValueChange={(e) => field().handleChange(e.value[0])}
onOpenChange={(details) => { if (!details.open) field().handleBlur(); }}>
<ComboboxLabel>Country</ComboboxLabel>
<ComboboxInputTrigger placeholder="Search country..." />
<ComboboxContent>
<Index each={countries.items}>
{(item) => <ComboboxItem item={item()}>{item().label}</ComboboxItem>}
</Index>
</ComboboxContent>
</Combobox>
</div>
)} />
<form.Field name="plan" children={(field) => (
<fieldset>
<div class="not-prose flex flex-col gap-2">
<legend class={labelVariants()}>Plan</legend>
<RadioGroup name={field().name} value={field().state.value}
onValueChange={(e) => field().handleChange(e.value ?? "")}
onBlur={field().handleBlur}>
<RadioGroupItem value="free">Free</RadioGroupItem>
<RadioGroupItem value="pro">Pro</RadioGroupItem>
<RadioGroupItem value="enterprise">Enterprise</RadioGroupItem>
</RadioGroup>
</div>
</fieldset>
)} />
<form.Field name="priority" children={(field) => (
<fieldset>
<div class="not-prose flex flex-col gap-2">
<legend class={labelVariants()}>Priority</legend>
<SegmentGroup name={field().name} value={field().state.value}
onValueChange={(e) => field().handleChange(e.value ?? "")}
onBlur={field().handleBlur}>
<SegmentGroupItem value="low">Low</SegmentGroupItem>
<SegmentGroupItem value="medium">Medium</SegmentGroupItem>
<SegmentGroupItem value="high">High</SegmentGroupItem>
</SegmentGroup>
</div>
</fieldset>
)} />
<form.Field name="rating" children={(field) => (
<RatingGroup name={field().name} value={field().state.value} count={5}
orientation="vertical"
onValueChange={(e) => field().handleChange(e.value)}
onBlur={field().handleBlur}>
<RatingGroupLabel>Rating</RatingGroupLabel>
</RatingGroup>
)} />
<form.Field name="volume" children={(field) => (
<Slider name={field().name} value={[field().state.value]}
min={0} max={100} step={1}
onValueChange={(e) => field().handleChange(e.value[0])}
onBlur={field().handleBlur}>
<SliderLabel>Volume: {field().state.value}%</SliderLabel>
<SliderControl><SliderThumb index={0} /></SliderControl>
</Slider>
)} />
<form.Field name="startDate" children={(field) => (
<DatePicker name={field().name} label="Start Date"
placeholder="Pick a date"
value={field().state.value ? [parseDate(field().state.value)] : []}
error={!!fieldError(field())}
onValueChange={(e) => field().handleChange(e.value[0] ? String(e.value[0]) : "")} />
)} />
<form.Field name="bio" children={(field) => (
<Textarea name={field().name} value={field().state.value} label="Bio"
placeholder="Tell us about yourself" error={fieldError(field())}
onBlur={field().handleBlur}
onInput={(e) => field().handleChange(e.currentTarget.value)} />
)} />
<form.Field name="skills" children={(field) => (
<div class="not-prose flex flex-col gap-1">
<TagsInput name={field().name} value={field().state.value}
label="Skills" placeholder="Type skill and press Enter"
onValueChange={(e) => field().handleChange(e.value)} />
</div>
)} />
<form.Field name="notifications" children={(field) => (
<Switch name={field().name} checked={field().state.value}
onCheckedChange={(e) => field().handleChange(!!e.checked)}
onBlur={field().handleBlur}>
<SwitchLabel>Enable email notifications</SwitchLabel>
</Switch>
)} />
<form.Field name="bold" children={(field) => (
<Toggle name={field().name} pressed={field().state.value}
onPressedChange={(pressed) => field().handleChange(!!pressed)}
onBlur={field().handleBlur}>
<BoldIcon />
Bold
</Toggle>
)} />
<form.Field name="alignment" children={(field) => (
<ToggleGroup value={[field().state.value]}
onValueChange={(e) => field().handleChange(e.value[0])}
onBlur={field().handleBlur}>
<ToggleGroupItem value="left">Left</ToggleGroupItem>
<ToggleGroupItem value="center">Center</ToggleGroupItem>
<ToggleGroupItem value="right">Right</ToggleGroupItem>
</ToggleGroup>
)} />
<form.Field name="accepted" children={(field) => (
<div class="not-prose flex flex-col gap-1">
<Checkbox name={field().name} checked={field().state.value}
invalid={!!fieldError(field())}
onCheckedChange={(e) => field().handleChange(!!e.checked)}
onBlur={field().handleBlur}>
<CheckboxLabel>I accept the terms and conditions</CheckboxLabel>
</Checkbox>
<FieldHint field={field()} />
</div>
)} />
<form.Subscribe
selector={(state) => ({ isSubmitting: state.isSubmitting })}
children={(state) => (
<Button type="submit" disabled={state().isSubmitting}>
{state().isSubmitting ? "Submitting..." : "Submit"}
</Button>
)}
/>
</form>
);
}