Basic Form

A simple login form with email validation and minimum password length


Code Svelte
1
2<script lang="ts">
3	import { Schema, pipe } from "effect";
4	import { SchemaForm, FormController, withField, withFormLayout } from "@kareyes/aether/forms";
5
6	const LoginSchema = pipe(
7		Schema.Struct({
8			email: pipe(
9				Schema.String,
10				Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/),
11				Schema.annotations({ message: () => "Please enter a valid email" }),
12				withField({
13					label: "Email",
14					inputType: "email",
15					placeholder: "you@example.com"
16				})
17			),
18			password: pipe(
19				Schema.String,
20				Schema.minLength(8),
21				withField({
22					label: "Password",
23					inputType: "password",
24					placeholder: "Min 8 characters"
25				})
26			)
27		}),
28		withFormLayout({
29			columns: 1,
30			sections: [{ id: "login", title: "Login" }]
31		})
32	);
33
34	const controller = new FormController(LoginSchema, {
35		validateOnBlur: true,
36		validateOnChange: false,
37		initialValues: { email: "", password: "" }
38	});
39
40	async function handleSubmit(data) {
41		console.log("Login submitted:", data);
42	}
43</script>
44
45<SchemaForm
46	{controller}
47	onSubmit={handleSubmit}
48	submitText="Sign In"
49/>

Multi-Section Form

A profile form with multiple sections using the card variant

Personal Information
Contact Details

Code Svelte
1
2<script lang="ts">
3	import { Schema, pipe } from "effect";
4	import { SchemaForm, FormController, withField, withFormLayout } from "@kareyes/aether/forms";
5
6	const ProfileSchema = pipe(
7		Schema.Struct({
8			firstName: pipe(
9				Schema.String,
10				Schema.minLength(1),
11				withField({
12					label: "First Name",
13					placeholder: "John",
14					section: "personal",
15					colSpan: 6
16				})
17			),
18			lastName: pipe(
19				Schema.String,
20				Schema.minLength(1),
21				withField({
22					label: "Last Name",
23					placeholder: "Doe",
24					section: "personal",
25					colSpan: 6
26				})
27			),
28			bio: pipe(
29				Schema.String,
30				withField({
31					label: "Bio",
32					inputType: "textarea",
33					placeholder: "Tell us about yourself...",
34					section: "personal"
35				})
36			),
37			email: pipe(
38				Schema.String,
39				Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/),
40				withField({
41					label: "Email",
42					inputType: "email",
43					section: "contact",
44					colSpan: 6
45				})
46			),
47			phone: pipe(
48				Schema.String,
49				withField({
50					label: "Phone",
51					inputType: "tel",
52					mask: "phone",
53					placeholder: "(555) 123-4567",
54					section: "contact",
55					colSpan: 6
56				})
57			)
58		}),
59		withFormLayout({
60			columns: 12,
61			sections: [
62				{ id: "personal", title: "Personal Information", order: 1 },
63				{ id: "contact", title: "Contact Details", order: 2 }
64			]
65		})
66	);
67
68	const controller = new FormController(ProfileSchema, {
69		validateOnBlur: true
70	});
71</script>
72
73<SchemaForm
74	{controller}
75	onSubmit={(data) => console.log("Profile:", data)}
76	sectionVariant="card"
77	submitText="Save Profile"
78/>

Multi-Step Form

A wizard-style registration form with step validation and navigation

Account
Create your login
Profile
Personal details
Finish
Preferences

Account

Create your login

Account


Code Svelte
1
2<script lang="ts">
3	import { Schema, pipe } from "effect";
4	import { SchemaForm, FormController, withField, withFormLayout } from "@kareyes/aether/forms";
5
6	const RegistrationSchema = pipe(
7		Schema.Struct({
8			email: pipe(
9				Schema.String,
10				Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/),
11				withField({ label: "Email", inputType: "email", step: 1, section: "account" })
12			),
13			password: pipe(
14				Schema.String,
15				Schema.minLength(8),
16				withField({ label: "Password", inputType: "password", step: 1, section: "account" })
17			),
18			firstName: pipe(
19				Schema.String,
20				Schema.minLength(1),
21				withField({ label: "First Name", step: 2, section: "profile", colSpan: 6 })
22			),
23			lastName: pipe(
24				Schema.String,
25				Schema.minLength(1),
26				withField({ label: "Last Name", step: 2, section: "profile", colSpan: 6 })
27			),
28			role: pipe(
29				Schema.String,
30				withField({
31					label: "Role",
32					inputType: "select",
33					step: 2,
34					section: "profile",
35					options: [
36						{ value: "developer", label: "Developer" },
37						{ value: "designer", label: "Designer" },
38						{ value: "manager", label: "Manager" }
39					]
40				})
41			),
42			newsletter: pipe(
43				Schema.Boolean,
44				withField({ label: "Subscribe to newsletter", step: 3, section: "preferences" })
45			)
46		}),
47		withFormLayout({
48			columns: 12,
49			sections: [
50				{ id: "account", title: "Account" },
51				{ id: "profile", title: "Profile" },
52				{ id: "preferences", title: "Preferences" }
53			],
54			steps: [
55				{ step: 1, title: "Account", description: "Create your login" },
56				{ step: 2, title: "Profile", description: "Personal details" },
57				{ step: 3, title: "Finish", description: "Preferences" }
58			]
59		})
60	);
61
62	const controller = new FormController(RegistrationSchema, {
63		validateOnBlur: true,
64		initialValues: { newsletter: false }
65	});
66</script>
67
68<SchemaForm
69	{controller}
70	onSubmit={(data) => console.log("Registered:", data)}
71	showStepIndicator={true}
72	nextText="Continue"
73	prevText="Go Back"
74	submitText="Complete Registration"
75/>

Input Types

All supported input types: text, email, password, number, url, tel, textarea, select, radio, switch, and checkbox


Code Svelte
1
2<script lang="ts">
3	import { Schema, pipe } from "effect";
4	import { SchemaForm, FormController, withField, withFormLayout } from "@kareyes/aether/forms";
5
6	const InputShowcaseSchema = pipe(
7		Schema.Struct({
8			fullName: pipe(
9				Schema.String,
10				withField({ label: "Full Name", inputType: "text", placeholder: "John Doe", colSpan: 6 })
11			),
12			email: pipe(
13				Schema.String,
14				withField({ label: "Email", inputType: "email", placeholder: "you@example.com", colSpan: 6 })
15			),
16			password: pipe(
17				Schema.String,
18				withField({ label: "Password", inputType: "password", colSpan: 6 })
19			),
20			age: pipe(
21				Schema.Number,
22				withField({ label: "Age", inputType: "number", colSpan: 6 })
23			),
24			website: pipe(
25				Schema.String,
26				withField({ label: "Website", inputType: "url", placeholder: "https://example.com", colSpan: 6 })
27			),
28			phone: pipe(
29				Schema.String,
30				withField({ label: "Phone", inputType: "tel", mask: "phone", colSpan: 6 })
31			),
32			bio: pipe(
33				Schema.String,
34				withField({ label: "Biography", inputType: "textarea", placeholder: "Tell us about yourself..." })
35			),
36			country: pipe(
37				Schema.String,
38				withField({
39					label: "Country",
40					inputType: "select",
41					colSpan: 6,
42					options: [
43						{ value: "us", label: "United States" },
44						{ value: "uk", label: "United Kingdom" },
45						{ value: "ca", label: "Canada" },
46						{ value: "au", label: "Australia" }
47					]
48				})
49			),
50			priority: pipe(
51				Schema.String,
52				withField({
53					label: "Priority",
54					inputType: "radio",
55					colSpan: 6,
56					options: [
57						{ value: "low", label: "Low" },
58						{ value: "medium", label: "Medium" },
59						{ value: "high", label: "High" }
60					]
61				})
62			),
63			notifications: pipe(
64				Schema.Boolean,
65				withField({ label: "Enable notifications", inputType: "switch", colSpan: 6 })
66			),
67			acceptTerms: pipe(
68				Schema.Boolean,
69				withField({ label: "I accept the terms and conditions", inputType: "checkbox", colSpan: 6 })
70			)
71		}),
72		withFormLayout({
73			columns: 12,
74			sections: [{ id: "main", title: "All Input Types" }]
75		})
76	);
77
78	const controller = new FormController(InputShowcaseSchema);
79</script>
80
81<SchemaForm
82	{controller}
83	onSubmit={(data) => console.log("Data:", data)}
84	submitText="Submit"
85/>

Validation

Schema-level validation with custom messages, pattern matching, and required checkboxes

3-20 characters, letters, numbers, underscores only

Minimum 8 characters


Code Svelte
1
2<script lang="ts">
3	import { Schema, pipe } from "effect";
4	import {
5		SchemaForm, FormController, withField, withFormLayout,
6		RequiredCheckbox, requiredCheckbox
7	} from "@kareyes/aether/forms";
8
9	const ValidationSchema = pipe(
10		Schema.Struct({
11			username: pipe(
12				Schema.String,
13				Schema.minLength(3),
14				Schema.maxLength(20),
15				Schema.pattern(/^[a-zA-Z0-9_]+$/),
16				Schema.annotations({
17					message: () => "Username must be 3-20 characters (letters, numbers, underscores)"
18				}),
19				withField({
20					label: "Username",
21					placeholder: "john_doe",
22					description: "3-20 characters, letters, numbers, underscores only",
23					colSpan: 6
24				})
25			),
26			email: pipe(
27				Schema.String,
28				Schema.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]+$/),
29				Schema.annotations({ message: () => "Please enter a valid email" }),
30				withField({ label: "Email", inputType: "email", colSpan: 6 })
31			),
32			password: pipe(
33				Schema.String,
34				Schema.minLength(8),
35				Schema.annotations({ message: () => "Password must be at least 8 characters" }),
36				withField({
37					label: "Password",
38					inputType: "password",
39					description: "Minimum 8 characters"
40				})
41			),
42			acceptTerms: pipe(
43				RequiredCheckbox,
44				withField({ label: "I accept the terms and conditions", inputType: "checkbox" })
45			),
46			acceptPrivacy: pipe(
47				requiredCheckbox("You must accept the privacy policy"),
48				withField({ label: "I accept the privacy policy", inputType: "checkbox" })
49			)
50		}),
51		withFormLayout({
52			columns: 12,
53			sections: [{ id: "main", title: "Validation Examples" }]
54		})
55	);
56
57	const controller = new FormController(ValidationSchema, {
58		validateOnBlur: true,
59		validateOnChange: false
60	});
61</script>
62
63<SchemaForm
64	{controller}
65	onSubmit={(data) => console.log("Valid data:", data)}
66	submitText="Register"
67/>

Section Variants - Card

Each section rendered inside a card container

Personal Info
Contact Info

Code Svelte
1
2<script lang="ts">
3	import { Schema, pipe } from "effect";
4	import { SchemaForm, FormController, withField, withFormLayout } from "@kareyes/aether/forms";
5
6	const SectionSchema = pipe(
7		Schema.Struct({
8			firstName: pipe(
9				Schema.String,
10				withField({ label: "First Name", section: "personal", colSpan: 6 })
11			),
12			lastName: pipe(
13				Schema.String,
14				withField({ label: "Last Name", section: "personal", colSpan: 6 })
15			),
16			email: pipe(
17				Schema.String,
18				withField({ label: "Email", inputType: "email", section: "contact" })
19			),
20			phone: pipe(
21				Schema.String,
22				withField({ label: "Phone", inputType: "tel", section: "contact" })
23			)
24		}),
25		withFormLayout({
26			columns: 12,
27			sections: [
28				{ id: "personal", title: "Personal Info", order: 1 },
29				{ id: "contact", title: "Contact Info", order: 2 }
30			]
31		})
32	);
33
34	// Change sectionVariant to "default", "card", or "collapsible"
35	const controller = new FormController(SectionSchema);
36</script>
37
38<!-- Default variant -->
39<SchemaForm {controller} sectionVariant="default" />
40
41<!-- Card variant -->
42<SchemaForm {controller} sectionVariant="card" />
43
44<!-- Collapsible variant -->
45<SchemaForm {controller} sectionVariant="collapsible" />

Section Variants - Collapsible

Accordion-style collapsible sections

Custom Footer

Override the default footer using the footer snippet. The snippet receives isSubmitting, isValid, isFirstStep, isLastStep, handleSubmit, handleNext, and handlePrev.

URL-friendly identifier

Ready to publish


Code Svelte
1
2<script lang="ts">
3	import { Schema, pipe } from "effect";
4	import { SchemaForm, FormController, withField, withFormLayout } from "@kareyes/aether/forms";
5	import { Button } from "@kareyes/aether";
6
7    // --- Custom Footer ---
8    const CustomFooterSchema = pipe(
9        Schema.Struct({
10            title: pipe(
11                Schema.String,
12                Schema.minLength(1),
13                withField({ label: "Title", colSpan: 6 }),
14            ),
15            slug: pipe(
16                Schema.String,
17                Schema.minLength(1),
18                withField({
19                    label: "Slug",
20                    colSpan: 6,
21                    description: "URL-friendly identifier",
22                }),
23            ),
24            body: pipe(
25                Schema.String,
26                withField({ label: "Body", inputType: "textarea" }),
27            ),
28        }),
29        withFormLayout({
30            columns: 12,
31            sections: [{ id: "main", title: "New Post" }],
32        }),
33    );
34
35    const customFooterController = new FormController(CustomFooterSchema, {
36        validateOnBlur: true,
37    });
38</script>
39
40	<SchemaForm
41		controller={customFooterController}
42		onSubmit={(data) => console.log("Post:", data)}
43	>
44		{#snippet footer({ isSubmitting, isValid, handleSubmit })}
45			<div class="flex items-center justify-between pt-4 border-t border-border">
46				<p class="text-sm text-muted-foreground">
47					{isValid ? "Ready to publish" : "Fix errors above"}
48				</p>
49				<div class="flex gap-2">
50					<Button
51						variant="outline"
52						type="button"
53						onclick={() => customFooterController.reset()}
54					>
55						Discard
56					</Button>
57					<Button
58						variant="outline"
59						type="button"
60						onclick={handleSubmit}
61						disabled={isSubmitting}
62					>
63						Save Draft
64					</Button>
65					<Button
66						type="button"
67						onclick={handleSubmit}
68						disabled={isSubmitting || !isValid}
69					>
70						{isSubmitting ? "Publishing…" : "Publish"}
71					</Button>
72				</div>
73			</div>
74		{/snippet}
75	</SchemaForm>