# Signup Form

> A full account creation form with name, country, birthday, email, and password fields and live password rules.

A shadcn/ui block from the [@esuiss](https://suiss.app/ui?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/esuiss/signup-form).

- Page, with a live preview: https://shadcn.exchange/r/esuiss/signup-form
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/esuiss/signup-form-light-d136304d8e2dedf9.png
- Registry item: https://suiss.app/ui/r/new-york-v4/signup-form.json
- Categories: blocks, signup
- Listed: 2026-10-20, source last changed 2026-10-20

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/esuiss/signup-form.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `@esuiss/button`, `@esuiss/input`, `@esuiss/native-select`

## Files

### `components/blocks/signup-form.tsx`

```tsx
"use client"

import * as React from "react"

import { IconPlaceholder } from "@/app/(create)/components/icon-placeholder"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { NativeSelect, NativeSelectOption } from "@/components/ui/native-select"

const EMAIL = /.+@.+\..+/
const UPPERCASE = /[A-Z]/
const LOWERCASE = /[a-z]/
const DIGIT = /\d/

const stackClass =
  "overflow-hidden rounded-xl border border-input bg-background transition-[border-color,background-color] duration-200 has-focus-visible:border-ring has-focus-visible:ring-3 has-focus-visible:ring-ring/50 data-invalid:border-destructive data-invalid:bg-destructive/5 dark:data-invalid:bg-destructive/10 *:not-first:border-t *:not-first:border-t-input"

const inputClass =
  "h-13 rounded-none bg-transparent px-4 text-lg focus-visible:border-transparent focus-visible:ring-0 aria-invalid:ring-0 aria-invalid:border-x-transparent aria-invalid:border-b-transparent aria-invalid:bg-transparent dark:aria-invalid:bg-transparent"

const rules = [
  {
    label: "At least 8 characters",
    test: (value: string) => value.length >= 8,
  },
  {
    label: "Upper and lowercase letters",
    test: (value: string) => UPPERCASE.test(value) && LOWERCASE.test(value),
  },
  { label: "At least one number", test: (value: string) => DIGIT.test(value) },
]

const strong = (value: string) => rules.every((rule) => rule.test(value))

function PasswordRules({ password }: { password: string }) {
  return (
    <ul className="flex flex-col gap-1.5 text-start">
      {rules.map((rule) => {
        const met = rule.test(password)
        return (
          <li
            key={rule.label}
            data-met={met ? "" : undefined}
            className="group/rule flex items-center gap-2 text-sm text-muted-foreground data-met:text-foreground"
          >
            <span className="flex size-4 items-center justify-center rounded-full border-[1.5px] border-border text-white transition-[background-color,border-color] duration-250 group-data-met/rule:border-transparent group-data-met/rule:bg-[color-mix(in_oklab,var(--color-green-500),var(--foreground)_25%)] dark:group-data-met/rule:bg-[color-mix(in_oklab,var(--color-green-500),black_35%)]">
              <IconPlaceholder
                lucide="CheckIcon"
                tabler="IconCheck"
                hugeicons="Tick02Icon"
                phosphor="CheckIcon"
                remixicon="RiCheckLine"
                className="size-2.5 scale-0 transition-[scale] duration-250 ease-[cubic-bezier(0.3,1.25,0.5,1)] group-data-met/rule:scale-100 motion-reduce:transition-none"
              />
            </span>
            {rule.label}
            <span className="sr-only">{met ? "(met)" : "(not met)"}</span>
          </li>
        )
      })}
    </ul>
  )
}

export function SignupForm() {
  const idPrefix = React.useId()
  const [form, setForm] = React.useState({
    first: "",
    last: "",
    country: "United States",
    birthday: "",
    email: "",
    password: "",
    confirm: "",
  })
  const [tried, setTried] = React.useState(false)
  const [done, setDone] = React.useState(false)

  const update =
    (key: keyof typeof form) =>
    (event: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) =>
      setForm((current) => ({ ...current, [key]: event.target.value }))

  const mismatch = tried && !!form.confirm && form.password !== form.confirm
  const valid =
    !!form.first &&
    !!form.last &&
    !!form.birthday &&
    EMAIL.test(form.email) &&
    strong(form.password) &&
    form.password === form.confirm

  if (done) {
    return (
      <section className="flex min-h-215 items-center justify-center p-6">
        <div className="flex max-w-105 flex-col items-center gap-3.5 text-center transition-[opacity,translate] duration-1000 ease-[cubic-bezier(0.32,0.72,0,1)] starting:translate-y-4.5 starting:opacity-0 motion-reduce:transition-none">
          <span className="flex size-19 items-center justify-center rounded-full bg-[color-mix(in_oklab,var(--color-green-500),var(--foreground)_25%)] text-white dark:bg-[color-mix(in_oklab,var(--color-green-500),black_35%)]">
            <IconPlaceholder
              lucide="CheckIcon"
              tabler="IconCheck"
              hugeicons="Tick02Icon"
              phosphor="CheckIcon"
              remixicon="RiCheckLine"
              className="size-9.5"
            />
          </span>
          <h1 className="text-4xl font-semibold tracking-tight">
            Your suiss Account is ready.
          </h1>
          <p className="text-lg text-muted-foreground">
            We sent a verification code to {form.email}.
          </p>
          <button
            type="button"
            onClick={() => {
              setDone(false)
              setTried(false)
            }}
            className="rounded-xs text-lg text-primary outline-none hover:underline focus-visible:ring-3 focus-visible:ring-ring/50"
          >
            Start over ›
          </button>
        </div>
      </section>
    )
  }

  return (
    <section className="px-6 py-9">
      <form
        noValidate
        onSubmit={(event) => {
          event.preventDefault()
          setTried(true)
          if (valid) setDone(true)
        }}
        className="mx-auto flex w-115 max-w-full flex-col text-center"
      >
        <h1 className="text-4xl font-semibold tracking-tight text-balance md:text-5xl">
          Create your suiss Account
        </h1>
        <p className="mt-2.5 text-lg text-balance text-muted-foreground">
          One suiss Account is all you need to access every suiss service.{" "}
          <a
            href="#learn"
            className="text-primary underline decoration-1 underline-offset-3"
          >
            Learn more ›
          </a>
        </p>
        <fieldset className="mt-5.5 flex flex-col gap-2 text-start">
          <legend className="mb-2 text-sm font-semibold">Your name</legend>
          <div className="grid grid-cols-2 gap-3">
            <div className={stackClass}>
              <Input
                value={form.first}
                onChange={update("first")}
                placeholder="First name"
                aria-label="First name"
                autoComplete="given-name"
                className={inputClass}
              />
            </div>
            <div className={stackClass}>
              <Input
                value={form.last}
                onChange={update("last")}
                placeholder="Last name"
                aria-label="Last name"
                autoComplete="family-name"
                className={inputClass}
              />
            </div>
          </div>
        </fieldset>
        <fieldset className="mt-5.5 flex flex-col text-start">
          <legend className="mb-2 text-sm font-semibold">
            Country or region and birthday
          </legend>
          <div className={stackClass}>
            <NativeSelect
              value={form.country}
              onChange={update("country")}
              aria-label="Country or region"
              className="w-full [&_select]:h-13 [&_select]:rounded-none [&_select]:bg-transparent [&_select]:ps-4 [&_select]:text-lg [&_select]:focus-visible:border-transparent [&_select]:focus-visible:ring-0 [&_select]:aria-invalid:ring-0"
            >
              {[
                "United States",
                "Germany",
                "Netherlands",
                "United Kingdom",
              ].map((country) => (
                <NativeSelectOption key={country} value={country}>
                  {country}
                </NativeSelectOption>
              ))}
            </NativeSelect>
            <Input
              value={form.birthday}
              onChange={update("birthday")}
              placeholder="Birthday (MM/DD/YYYY)"
              aria-label="Birthday"
              autoComplete="bday"
              inputMode="numeric"
              className={inputClass}
            />
          </div>
        </fieldset>
        <fieldset className="mt-5.5 flex flex-col text-start">
          <legend className="mb-2 text-sm font-semibold">suiss Account</legend>
          <div className={stackClass}>
            <Input
              type="email"
              value={form.email}
              onChange={update("email")}
              placeholder="name@example.com"
              aria-label="Email"
              aria-describedby={`${idPrefix}-email-hint`}
              autoComplete="email"
              className={inputClass}
            />
          </div>
          <p
            id={`${idPrefix}-email-hint`}
            className="mt-1.5 text-xs text-muted-foreground"
          >
            This will be your new suiss Account.
          </p>
        </fieldset>
        <fieldset className="mt-5.5 flex flex-col text-start">
          <legend className="mb-2 text-sm font-semibold">Password</legend>
          <div data-invalid={mismatch ? "" : undefined} className={stackClass}>
            <Input
              type="password"
              value={form.password}
              onChange={update("password")}
              placeholder="Password"
              aria-label="Password"
              autoComplete="new-password"
              className={inputClass}
            />
            <Input
              type="password"
              value={form.confirm}
              onChange={update("confirm")}
              placeholder="Confirm password"
              aria-label="Confirm password"
              aria-invalid={mismatch || undefined}
              autoComplete="new-password"
              className={inputClass}
            />
          </div>
          <div
            data-open={form.password ? "" : undefined}
            className="grid grid-rows-[0fr] transition-[grid-template-rows] duration-400 ease-[cubic-bezier(0.32,0.72,0,1)] data-open:grid-rows-[1fr] motion-reduce:transition-none"
          >
            <div className="min-h-0 overflow-hidden">
              <div className="px-1 pt-3.5">
                <PasswordRules password={form.password} />
              </div>
            </div>
          </div>
        </fieldset>
        <p
          aria-live="polite"
          className="mt-3.5 min-h-5 text-sm text-destructive"
        >
          {tried && !valid ? "Please fill in every field correctly." : ""}
        </p>
        <Button type="submit" size="lg" className="mt-3 min-w-40 self-center">
          Continue
        </Button>
      </form>
    </section>
  )
}
```
