Skip to content
LuoForge/Tungsten
← All recipes

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

* required

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>
  );
}