Sign Up Form

Registration form with name, email, password with live strength meter, password confirmation, and a required terms checkbox. Cross-field validation with Zod's .refine() ensures both passwords match before submission.

Create an account

Start your free trial — no credit card required

Already have an account? Sign in

import { SignUpForm } from "@aetherstack/patterns"

Installation

terminal
pnpm add react-hook-form zod @hookform/resolvers

Import

import.tsx
import { useForm } from "react-hook-form"
import { zodResolver } from "@hookform/resolvers/zod"
import { z } from "zod"
import { Button, Input, Label, Checkbox } from "@aetherstack/ui"

Usage

sign-up-form.tsx
const schema = z
  .object({
    name: z.string().min(2, "Name must be at least 2 characters"),
    email: z.string().email("Enter a valid email address"),
    password: z
      .string()
      .min(8, "At least 8 characters")
      .regex(/[A-Z]/, "Must contain one uppercase letter")
      .regex(/[0-9]/, "Must contain one number"),
    confirmPassword: z.string(),
    terms: z.literal(true, {
      errorMap: () => ({ message: "You must accept the terms" }),
    }),
  })
  .refine((d) => d.password === d.confirmPassword, {
    path: ["confirmPassword"],
    message: "Passwords do not match",
  })

export function SignupForm() {
  const { register, handleSubmit, watch, formState: { errors, isSubmitting } } =
    useForm({ resolver: zodResolver(schema) })

  const password = watch("password", "")

  return (
    <form onSubmit={handleSubmit(onSubmit)} noValidate>
      <Label htmlFor="name">Full name</Label>
      <Input id="name" {...register("name")} />
      {errors.name && <p>{errors.name.message}</p>}

      <Label htmlFor="email">Email</Label>
      <Input id="email" type="email" {...register("email")} />
      {errors.email && <p>{errors.email.message}</p>}

      <Label htmlFor="password">Password</Label>
      <Input id="password" type="password" {...register("password")} />
      <PasswordStrength value={password} />

      <Label htmlFor="confirmPassword">Confirm password</Label>
      <Input id="confirmPassword" type="password" {...register("confirmPassword")} />
      {errors.confirmPassword && <p>{errors.confirmPassword.message}</p>}

      <Checkbox id="terms" {...register("terms")} />
      <Label htmlFor="terms">I agree to the Terms</Label>
      {errors.terms && <p>{errors.terms.message}</p>}

      <Button type="submit" disabled={isSubmitting}>Create account</Button>
    </form>
  )
}

Props

PropTypeDefaultDescription
schema (Zod .refine)ZodEffectsCross-field refine validates that password and confirmPassword are equal. The error is attached to the confirmPassword field path.
watchUseFormWatch<FormValues>Subscribes to the password field value in real time — powers the PasswordStrength indicator without triggering full re-validation.
PasswordStrength({ value: string }) => ReactNodeVisual bar that scores the password by length, uppercase, digit, and special character rules. Score ranges from 0–4.
terms (z.literal(true))booleanUsing z.literal(true) means the checkbox must be explicitly checked — an unchecked false value fails validation with a custom error message.

* Required

Accessibility

  • All inputs are associated with explicit labels via htmlFor/id.
  • aria-invalid is set on inputs with active validation errors.
  • Error messages are placed directly below their input for correct reading order.
  • The password strength bar is a visual aid only — strength text is also rendered as plain text for screen readers.
  • The terms checkbox error is indented to align visually with its label.