Signup form
Account creation form demonstrating cross-field validation: the confirm-password field is checked against password via FormProvider’s form-level validate(values).
Preview
Source
apps/docs/app/recipes/signup/recipe.tsx'use client';
import { Button, FormField, FormProvider, Input, useForm } from '@hey-mike/tungsten';
const isEmail = (v: unknown) =>
typeof v === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) ? undefined : 'Enter a valid email';
const min8 = (v: unknown) =>
typeof v === 'string' && v.length >= 8 ? undefined : 'Use at least 8 characters';
// Cross-field validation lives at the form level (ADR-0003) — per-field
// validators can't see sibling values, so confirm-vs-password is checked here.
const matchPasswords = (values: Record<string, unknown>): Record<string, string> => {
if (values.password && values.confirm && values.password !== values.confirm) {
return { confirm: 'Passwords must match' };
}
return {};
};
function SignupFields() {
const form = useForm();
return (
<form onSubmit={form.handleSubmit} className="flex w-full max-w-sm flex-col gap-4" noValidate>
<p className="text-ink-3 text-2xs font-mono uppercase tracking-label">
<span className="text-error-text">*</span> required
</p>
<FormField name="email" label="Email" required validate={isEmail}>
<Input type="email" placeholder="you@example.com" autoComplete="email" />
</FormField>
<FormField name="password" label="Password" required validate={min8}>
<Input type="password" autoComplete="new-password" />
</FormField>
<FormField name="confirm" label="Confirm password" required validate={min8}>
<Input type="password" autoComplete="new-password" />
</FormField>
<Button type="submit" variant="brand" size="md">
Create account
</Button>
</form>
);
}
export default function SignupRecipe() {
return (
<FormProvider validate={matchPasswords}>
<SignupFields />
</FormProvider>
);
}
'use client';
import { Button, FormField, FormProvider, Input, useForm } from '@hey-mike/tungsten';
const isEmail = (v: unknown) =>
typeof v === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v) ? undefined : 'Enter a valid email';
const min8 = (v: unknown) =>
typeof v === 'string' && v.length >= 8 ? undefined : 'Use at least 8 characters';
// Cross-field validation lives at the form level (ADR-0003) — per-field
// validators can't see sibling values, so confirm-vs-password is checked here.
const matchPasswords = (values: Record<string, unknown>): Record<string, string> => {
if (values.password && values.confirm && values.password !== values.confirm) {
return { confirm: 'Passwords must match' };
}
return {};
};
function SignupFields() {
const form = useForm();
return (
<form onSubmit={form.handleSubmit} className="flex w-full max-w-sm flex-col gap-4" noValidate>
<p className="text-ink-3 text-2xs font-mono uppercase tracking-label">
<span className="text-error-text">*</span> required
</p>
<FormField name="email" label="Email" required validate={isEmail}>
<Input type="email" placeholder="you@example.com" autoComplete="email" />
</FormField>
<FormField name="password" label="Password" required validate={min8}>
<Input type="password" autoComplete="new-password" />
</FormField>
<FormField name="confirm" label="Confirm password" required validate={min8}>
<Input type="password" autoComplete="new-password" />
</FormField>
<Button type="submit" variant="brand" size="md">
Create account
</Button>
</form>
);
}
export default function SignupRecipe() {
return (
<FormProvider validate={matchPasswords}>
<SignupFields />
</FormProvider>
);
}