# Signup Form

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

Signup Form is an open-source block from suiss/ui for React, React Native, SwiftUI and Jetpack Compose. The @esuiss/ui CLI copies its source into your project, so you own the code.

Source: https://ui.suiss.app/docs/blocks/signup-form

**React**

**Example**

```tsx title="components/blocks/signup-form.tsx"
"use client"

import { CheckIcon } from "@phosphor-icons/react"
import * as React from "react"

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%)]">
              <CheckIcon
                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%)]">
            <CheckIcon 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>
  )
}
```

A full account creation form with name, country, birthday, email and a password with confirmation, grouped into labeled sections. A checklist of password rules appears as the user types, and mismatched passwords turn the fields red. Submitting a valid form shows a success view. Use it when you need more than an email and password at signup.

## Installation

### CLI

```bash title="Terminal"
npx shadcn@latest add @esuiss/signup-form
```

Works in any project with a `components.json`. See [Installation](https://ui.suiss.app/docs/installation#add-components) for the one-line registry setup. Or use the suiss/ui CLI:

```bash title="Terminal"
npx @esuiss/ui@latest add blocks signup-form
```

### Manual

1. Install the dependencies:

```bash title="Terminal"
npm install @phosphor-icons/react
```

2. Add the pieces it builds on: [Button](https://ui.suiss.app/docs/components/button), [Input](https://ui.suiss.app/docs/components/input), [Native Select](https://ui.suiss.app/docs/components/native-select).

3. Copy the source into `components/blocks/signup-form.tsx`:

```tsx title="components/blocks/signup-form.tsx"
"use client"

import { CheckIcon } from "@phosphor-icons/react"
import * as React from "react"

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%)]">
              <CheckIcon
                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%)]">
            <CheckIcon 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>
  )
}
```

4. Update the import paths to match your project.

## Usage

```tsx
import { SignupForm } from "@/components/blocks/signup-form"
```

```tsx
<SignupForm />
```

Replace the success branch with your registration API call. Edit `rules` for the password checklist and the country list inline. The birthday is free text and is never parsed, so swap in a date picker or validate it before sending.

## Notes

Every text field has an autocomplete value so browsers and password managers can fill it, and the form runs its own validation instead of the browser's.

**React Native, SwiftUI, Jetpack Compose**

![Preview](https://ui.suiss.app/previews/blocks-signup-form--default.light.webp)

```tsx title="uniwind/components/blocks/signup-form.tsx"
import { cn } from "cn"
import { CheckIcon } from "phosphor-react-native"
import * as React from "react"
import { Pressable, View } from "react-native"
import {
  FadeIn,
  FadeInDown,
  ReduceMotion,
  ZoomIn,
} from "react-native-reanimated"
import { Picker, PickerItem } from "@/components/patterns/picker"
import { AnimatedView } from "@/components/ui/animated-view"
import { BottomBar, BottomBarContent } from "@/components/ui/bottom-bar"
import { Button } from "@/components/ui/button"
import { Icon } from "@/components/ui/icon"
import {
  InputAccessoryBar,
  InputAccessoryBarInput,
} from "@/components/ui/input-accessory-bar"
import { KeyboardScrollView } from "@/components/ui/keyboard-scroll-view"
import { Screen } from "@/components/ui/screen"
import { Text } from "@/components/ui/text"

const EMAIL = /.+@.+\..+/
const UPPERCASE = /[A-Z]/
const LOWERCASE = /[a-z]/
const DIGIT = /\d/
const SYMBOL = /[^A-Za-z0-9]/

const COUNTRIES = ["United States", "Germany", "Netherlands", "United Kingdom"]

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 STACK_CLASS =
  "overflow-hidden rounded-xl border border-input bg-background"

const INPUT_CLASS =
  "h-13 rounded-none border-transparent bg-transparent px-4 text-lg"

const strength = (value: string) =>
  RULES.filter((rule) => rule.test(value)).length + (SYMBOL.test(value) ? 1 : 0)

function PasswordRules({ password }: { password: string }) {
  return (
    <View className="gap-1.5">
      {RULES.map((rule) => {
        const met = rule.test(password)
        return (
          <View
            key={rule.label}
            accessible
            accessibilityLabel={`${rule.label} ${met ? "(met)" : "(not met)"}`}
            className="flex-row items-center gap-2"
          >
            <View
              className={cn(
                "size-4 items-center justify-center rounded-full border-[1.5px]",
                met ? "border-transparent bg-green-500" : "border-border"
              )}
            >
              {met ? (
                <AnimatedView
                  entering={ZoomIn.duration(250).reduceMotion(
                    ReduceMotion.System
                  )}
                >
                  <Icon
                    as={CheckIcon}
                    weight="bold"
                    className="size-2.5 text-primary-foreground"
                  />
                </AnimatedView>
              ) : null}
            </View>
            <Text
              className={cn(
                "text-sm",
                met ? "text-foreground" : "text-muted-foreground"
              )}
            >
              {rule.label}
            </Text>
          </View>
        )
      })}
    </View>
  )
}

function Legend({ children }: { children: React.ReactNode }) {
  return (
    <Text role="heading" className="mb-2 text-sm font-semibold">
      {children}
    </Text>
  )
}

type SignupFormValues = {
  firstName: string
  lastName: string
  country: string
  birthday: string
  email: string
  password: string
}

function SignupForm({
  onLearnMore,
  onSubmit,
}: {
  onLearnMore?: () => void
  onSubmit?: (values: SignupFormValues) => void
}) {
  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) => (value: string) =>
    setForm((current) => ({ ...current, [key]: value }))

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

  const submit = () => {
    setTried(true)
    if (!valid) return
    onSubmit?.({
      firstName: form.first,
      lastName: form.last,
      country: form.country,
      birthday: form.birthday,
      email: form.email,
      password: form.password,
    })
    setDone(true)
  }

  if (done) {
    return (
      <Screen>
        <View className="flex-1 items-center justify-center p-6">
          <AnimatedView
            entering={FadeInDown.duration(600).reduceMotion(
              ReduceMotion.System
            )}
            className="items-center gap-3.5"
          >
            <View className="size-19 items-center justify-center rounded-full bg-green-500">
              <Icon
                as={CheckIcon}
                weight="bold"
                className="size-9.5 text-primary-foreground"
              />
            </View>
            <Text role="heading" className="text-center text-3xl font-semibold">
              Your suiss Account is ready.
            </Text>
            <Text className="text-center text-lg text-muted-foreground">
              We sent a verification code to {form.email}.
            </Text>
            <Pressable
              role="link"
              hitSlop={8}
              onPress={() => {
                setDone(false)
                setTried(false)
              }}
              className="active:opacity-60"
            >
              <Text className="text-lg text-primary">Start over ›</Text>
            </Pressable>
          </AnimatedView>
        </View>
      </Screen>
    )
  }

  return (
    <Screen>
      <InputAccessoryBar>
        <KeyboardScrollView contentContainerClassName="px-6 pt-9 pb-6">
          <Text role="heading" className="text-center text-3xl font-semibold">
            Create your suiss Account
          </Text>
          <Text className="mt-2.5 text-center text-lg text-muted-foreground">
            One suiss Account is all you need to access every suiss service.{" "}
            <Text
              role="link"
              className="text-lg text-primary"
              onPress={onLearnMore}
            >
              Learn more ›
            </Text>
          </Text>
          <View className="mt-5.5">
            <Legend>Your name</Legend>
            <View className="flex-row gap-3">
              <View className={cn(STACK_CLASS, "flex-1")}>
                <InputAccessoryBarInput
                  value={form.first}
                  onChangeText={update("first")}
                  placeholder="First name"
                  accessibilityLabel="First name"
                  autoComplete="given-name"
                  textContentType="givenName"
                  className={INPUT_CLASS}
                />
              </View>
              <View className={cn(STACK_CLASS, "flex-1")}>
                <InputAccessoryBarInput
                  value={form.last}
                  onChangeText={update("last")}
                  placeholder="Last name"
                  accessibilityLabel="Last name"
                  autoComplete="family-name"
                  textContentType="familyName"
                  className={INPUT_CLASS}
                />
              </View>
            </View>
          </View>
          <View className="mt-5.5">
            <Legend>Country or region and birthday</Legend>
            <View className={STACK_CLASS}>
              <Picker
                value={form.country}
                onValueChange={update("country")}
                accessibilityLabel="Country or region"
                className="h-13 w-full self-stretch rounded-none border-transparent bg-transparent ps-4"
              >
                {COUNTRIES.map((country) => (
                  <PickerItem key={country} value={country}>
                    {country}
                  </PickerItem>
                ))}
              </Picker>
              <View className="h-px bg-input" />
              <InputAccessoryBarInput
                value={form.birthday}
                onChangeText={update("birthday")}
                placeholder="Birthday (MM/DD/YYYY)"
                accessibilityLabel="Birthday"
                autoComplete="birthdate-full"
                keyboardType="numbers-and-punctuation"
                className={INPUT_CLASS}
              />
            </View>
          </View>
          <View className="mt-5.5">
            <Legend>suiss Account</Legend>
            <View className={STACK_CLASS}>
              <InputAccessoryBarInput
                value={form.email}
                onChangeText={update("email")}
                placeholder="name@example.com"
                accessibilityLabel="Email"
                accessibilityHint="This will be your new suiss Account."
                autoComplete="email"
                textContentType="emailAddress"
                keyboardType="email-address"
                autoCapitalize="none"
                autoCorrect={false}
                className={INPUT_CLASS}
              />
            </View>
            <Text className="mt-1.5 text-xs text-muted-foreground">
              This will be your new suiss Account.
            </Text>
          </View>
          <View className="mt-5.5">
            <Legend>Password</Legend>
            <View
              className={cn(
                STACK_CLASS,
                mismatch &&
                  "border-destructive bg-destructive/5 dark:bg-destructive/10"
              )}
            >
              <InputAccessoryBarInput
                value={form.password}
                onChangeText={update("password")}
                placeholder="Password"
                accessibilityLabel="Password"
                autoComplete="new-password"
                textContentType="newPassword"
                secureTextEntry
                className={INPUT_CLASS}
              />
              <View
                className={cn("h-px", mismatch ? "bg-destructive" : "bg-input")}
              />
              <InputAccessoryBarInput
                value={form.confirm}
                onChangeText={update("confirm")}
                placeholder="Confirm password"
                accessibilityLabel="Confirm password"
                autoComplete="new-password"
                textContentType="newPassword"
                secureTextEntry
                className={INPUT_CLASS}
              />
            </View>
            {form.password ? (
              <AnimatedView
                entering={FadeIn.duration(300).reduceMotion(
                  ReduceMotion.System
                )}
                className="px-1 pt-3.5"
              >
                <PasswordRules password={form.password} />
              </AnimatedView>
            ) : null}
          </View>
          <Text
            accessibilityLiveRegion="polite"
            className="mt-3.5 min-h-5 text-center text-sm text-destructive"
          >
            {tried && !valid ? "Please fill in every field correctly." : ""}
          </Text>
        </KeyboardScrollView>
      </InputAccessoryBar>
      <BottomBar separator>
        <BottomBarContent>
          <Button size="lg" block onPress={submit}>
            <Text>Continue</Text>
          </Button>
        </BottomBarContent>
      </BottomBar>
    </Screen>
  )
}

export type { SignupFormValues }
export { SignupForm }
```

```swift title="swift/Blocks/SignupForm.swift"
import PhosphorSwift
import SwiftUI

public struct SignupFormValues: Equatable, Sendable {
  public var firstName: String
  public var lastName: String
  public var country: String
  public var birthday: String
  public var email: String
  public var password: String

  public init(firstName: String, lastName: String, country: String, birthday: String, email: String, password: String) {
    self.firstName = firstName
    self.lastName = lastName
    self.country = country
    self.birthday = birthday
    self.email = email
    self.password = password
  }
}

public struct SignupForm: View {
  struct Values: Sendable {
    var first = ""
    var last = ""
    var country: String? = "United States"
    var birthday = ""
    var email = ""
    var password = ""
    var confirm = ""
  }

  enum FocusField: Hashable {
    case first, last, birthday, email, password, confirm
  }

  static var countries: [String] { ["United States", "Germany", "Netherlands", "United Kingdom"] }

  private var onLearnMore: (() -> Void)?
  private var onSubmit: ((SignupFormValues) -> Void)?
  @State private var form: Values
  @State private var tried: Bool
  @State private var done: Bool
  @FocusState private var focus: FocusField?
  @Environment(\.accessibilityReduceMotion) private var reduceMotion

  public init(onSubmit: ((SignupFormValues) -> Void)? = nil, onLearnMore: (() -> Void)? = nil) {
    self.init(form: Values())
    self.onSubmit = onSubmit
    self.onLearnMore = onLearnMore
  }

  init(form: Values, tried: Bool = false, done: Bool = false) {
    _form = State(initialValue: form)
    _tried = State(initialValue: tried)
    _done = State(initialValue: done)
  }

  private var mismatch: Bool {
    tried && !form.confirm.isEmpty && form.password != form.confirm
  }

  private var valid: Bool {
    !form.first.isEmpty && !form.last.isEmpty && !form.birthday.isEmpty
      && form.email.wholeMatch(of: /.+@.+\..+/) != nil
      && SignupFormRules.strength(form.password) >= 3
      && form.password == form.confirm
  }

  public var body: some View {
    Screen {
      if done {
        SignupFormDone(email: form.email) {
          done = false
          tried = false
        }
        .transition(.opacity.combined(with: .offset(y: 18)))
      } else {
        content
        BottomBar {
          BottomBarContent {
            Button("Continue") {
              tried = true
              focus = nil
              if valid {
                onSubmit?(SignupFormValues(
                  firstName: form.first,
                  lastName: form.last,
                  country: form.country ?? "",
                  birthday: form.birthday,
                  email: form.email,
                  password: form.password
                ))
                done = true
              }
            }
            .buttonStyle(.variant(.default, size: .lg, block: true))
          }
        }
      }
    }
    .animation(reduceMotion ? nil : .timingCurve(0.32, 0.72, 0, 1, duration: 0.6), value: done)
  }

  private var content: some View {
    KeyboardScrollView {
      VStack(alignment: .leading, spacing: 22) {
        VStack(spacing: 10) {
          Text("Create your suiss Account")
            .scaledTextStyle(.xl2, weight: .bold)
          Text("One suiss Account is all you need to access every suiss service.")
            .scaledTextStyle(.lg)
            .foregroundStyle(EsuissColor.mutedForeground)
          Button("Learn more ›") { onLearnMore?() }
            .buttonStyle(SignupFormLinkStyle())
        }
        .multilineTextAlignment(.center)
        .fixedSize(horizontal: false, vertical: true)
        .frame(maxWidth: .infinity)
        section("Your name") {
          HStack(spacing: 12) {
            input(Text("First name"), text: $form.first, field: .first, content: .givenName)
            input(Text("Last name"), text: $form.last, field: .last, content: .familyName)
          }
        }
        section("Country or region and birthday") {
          OptionPicker(selection: $form.country, placeholder: "Country or region") {
            ForEach(Self.countries, id: \.self) { country in
              OptionPickerItem(verbatim: country, value: country)
            }
          }
          .accessibilityLabel(Text("Country or region"))
          input(Text("Birthday (MM/DD/YYYY)"), text: $form.birthday, field: .birthday, content: .birthdate, keyboard: .numbersAndPunctuation)
        }
        section("suiss Account") {
          input(Text(verbatim: "name@example.com"), text: $form.email, field: .email, content: .emailAddress, keyboard: .emailAddress)
          Text("This will be your new suiss Account.")
            .scaledTextStyle(.xs)
            .foregroundStyle(EsuissColor.mutedForeground)
        }
        section("Password") {
          input(Text("Password"), text: $form.password, field: .password, content: .newPassword, secure: true)
          input(Text("Confirm password"), text: $form.confirm, field: .confirm, content: .newPassword, secure: true)
            .invalid(mismatch)
          if !form.password.isEmpty {
            SignupFormRules(password: form.password)
              .padding(.horizontal, 4)
              .padding(.top, 6)
              .transition(.opacity.combined(with: .move(edge: .top)))
          }
        }
        .animation(reduceMotion ? nil : .timingCurve(0.32, 0.72, 0, 1, duration: 0.4), value: form.password.isEmpty)
        Text(tried && !valid ? "Please fill in every field correctly." : "")
          .scaledTextStyle(.xs)
          .foregroundStyle(EsuissColor.destructive)
          .frame(maxWidth: .infinity, minHeight: 20)
          .accessibilityHidden(!(tried && !valid))
      }
      .padding(.horizontal, 24)
      .padding(.top, 36)
      .padding(.bottom, 16)
    }
    .inputAccessoryBar(focus: $focus, order: [.first, .last, .birthday, .email, .password, .confirm])
  }

  private func section<Content: View>(_ legend: LocalizedStringKey, @ViewBuilder content: () -> Content) -> some View {
    VStack(alignment: .leading, spacing: 8) {
      Text(legend)
        .scaledFont(.sm, weight: .semibold)
        .accessibilityAddTraits(.isHeader)
      content()
    }
  }

  @ViewBuilder
  private func input(
    _ title: Text,
    text: Binding<String>,
    field: FocusField,
    content: UITextContentType,
    keyboard: UIKeyboardType = .default,
    secure: Bool = false
  ) -> some View {
    Group {
      if secure {
        SecureField(text: text, prompt: title) { title }
      } else {
        TextField(text: text, prompt: title) { title }
          .keyboardType(keyboard)
          .textInputAutocapitalization(keyboard == .emailAddress ? .never : .words)
          .autocorrectionDisabled()
      }
    }
    .textContentType(content)
    .submitLabel(field == .confirm ? .done : .next)
    .focused($focus, equals: field)
    .textFieldStyle(.input)
  }
}

private struct SignupFormRules: View {
  let password: String

  static var rules: [(LocalizedStringKey, (String) -> Bool)] {
    [
      ("At least 8 characters", { $0.count >= 8 }),
      ("Upper and lowercase letters", { $0.contains(where: \.isUppercase) && $0.contains(where: \.isLowercase) }),
      ("At least one number", { $0.contains(where: \.isNumber) }),
    ]
  }

  static func strength(_ value: String) -> Int {
    rules.filter { $0.1(value) }.count + (value.contains { !$0.isLetter && !$0.isNumber } ? 1 : 0)
  }

  @Environment(\.accessibilityReduceMotion) private var reduceMotion

  var body: some View {
    VStack(alignment: .leading, spacing: 6) {
      ForEach(Array(Self.rules.enumerated()), id: \.offset) { _, rule in
        let met = rule.1(password)
        HStack(spacing: 8) {
          ZStack {
            Circle()
              .strokeBorder(met ? Color.clear : EsuissColor.border, lineWidth: 1.5)
              .background(met ? Palette.green500 : .clear, in: Circle())
            Ph.check.bold
              .frame(width: 10, height: 10)
              .foregroundStyle(EsuissColor.primaryForeground)
              .scaleEffect(met ? 1 : 0)
          }
          .frame(width: 16, height: 16)
          Text(rule.0)
            .scaledFont(.sm)
            .foregroundStyle(met ? EsuissColor.foreground : EsuissColor.mutedForeground)
        }
        .animation(reduceMotion ? nil : .spring(response: 0.25, dampingFraction: 0.6), value: met)
        .accessibilityElement(children: .combine)
        .accessibilityValue(met ? Text("Met") : Text("Not met"))
      }
    }
  }
}

private struct SignupFormDone: View {
  let email: String
  let onReset: () -> Void

  var body: some View {
    VStack(spacing: 14) {
      Ph.check.bold
        .frame(width: 38, height: 38)
        .foregroundStyle(EsuissColor.primaryForeground)
        .frame(width: 76, height: 76)
        .background(Palette.green500, in: Circle())
        .accessibilityHidden(true)
      Text("Your suiss Account is ready.")
        .scaledTextStyle(.xl2, weight: .bold)
      Text("We sent a verification code to \(email).")
        .scaledTextStyle(.lg)
        .foregroundStyle(EsuissColor.mutedForeground)
      Button("Start over ›", action: onReset)
        .buttonStyle(SignupFormLinkStyle(style: .lg))
    }
    .multilineTextAlignment(.center)
    .fixedSize(horizontal: false, vertical: true)
    .padding(24)
    .frame(maxWidth: .infinity, maxHeight: .infinity)
  }
}

private struct SignupFormLinkStyle: ButtonStyle {
  var style: TextStyle = .base

  func makeBody(configuration: Configuration) -> some View {
    configuration.label
      .scaledFont(style)
      .foregroundStyle(EsuissColor.primary)
      .opacity(configuration.isPressed ? 0.8 : 1)
      .frame(minHeight: 32)
      .contentShape(Rectangle())
  }
}
```

```kt title="kotlin/blocks/SignupForm.kt"
package com.esuiss.ui.blocks

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.animateFloatAsState
import androidx.compose.animation.core.tween
import androidx.compose.animation.expandVertically
import androidx.compose.animation.shrinkVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.heightIn
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicText
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.semantics.LiveRegionMode
import androidx.compose.ui.semantics.Role
import androidx.compose.ui.semantics.contentDescription
import androidx.compose.ui.semantics.error
import androidx.compose.ui.semantics.heading
import androidx.compose.ui.semantics.liveRegion
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.stateDescription
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Bold
import com.adamglin.phosphoricons.bold.Check
import com.esuiss.ui.components.BottomBar
import com.esuiss.ui.components.BottomBarContent
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.ButtonSize
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.InputAccessoryBar
import com.esuiss.ui.components.InputAccessoryState
import com.esuiss.ui.components.KeyboardScrollView
import com.esuiss.ui.components.Screen
import com.esuiss.ui.components.inputAccessory
import com.esuiss.ui.components.rememberInputAccessoryState
import com.esuiss.ui.patterns.Picker
import com.esuiss.ui.patterns.PickerItemData
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Palette
import com.esuiss.ui.theme.Radius
import com.esuiss.ui.theme.Typography

private val EmailPattern = Regex(".+@.+\\..+")

private val RevealEasing = CubicBezierEasing(0.32f, 0.72f, 0f, 1f)

private val PopEasing = CubicBezierEasing(0.3f, 1.25f, 0.5f, 1f)

private class PasswordRule(val label: String, val test: (String) -> Boolean)

private val PasswordRules = listOf(
    PasswordRule("At least 8 characters") { it.length >= 8 },
    PasswordRule("Upper and lowercase letters") { value -> value.any { it.isUpperCase() } && value.any { it.isLowerCase() } },
    PasswordRule("At least one number") { value -> value.any { it.isDigit() } },
)

private fun strength(value: String): Int =
    PasswordRules.count { it.test(value) } + if (value.any { !it.isLetterOrDigit() }) 1 else 0

private val Countries = listOf("United States", "Germany", "Netherlands", "United Kingdom").map { PickerItemData(it, it) }

@Immutable
data class SignupFormValues(
    val firstName: String,
    val lastName: String,
    val country: String,
    val birthday: String,
    val email: String,
    val password: String,
)

@Composable
fun SignupForm(
    modifier: Modifier = Modifier,
    onSubmit: (SignupFormValues) -> Unit = {},
    onLearnMore: () -> Unit = {},
) {
    val colors = EsuissTheme.colors
    var first by rememberSaveable { mutableStateOf("") }
    var last by rememberSaveable { mutableStateOf("") }
    var country by rememberSaveable { mutableStateOf("United States") }
    var birthday by rememberSaveable { mutableStateOf("") }
    var email by rememberSaveable { mutableStateOf("") }
    var password by remember { mutableStateOf("") }
    var confirm by remember { mutableStateOf("") }
    var tried by rememberSaveable { mutableStateOf(false) }
    var done by rememberSaveable { mutableStateOf(false) }
    val accessory = rememberInputAccessoryState()

    val mismatch = tried && confirm.isNotEmpty() && password != confirm
    val valid = first.isNotEmpty() && last.isNotEmpty() && birthday.isNotEmpty() &&
        EmailPattern.matches(email) && strength(password) >= 3 && password == confirm

    fun submit() {
        tried = true
        if (!valid) return
        onSubmit(SignupFormValues(first, last, country, birthday, email, password))
        done = true
    }

    Screen(modifier) {
        if (done) {
            Success(
                title = "Your suiss Account is ready.",
                description = "We sent a verification code to $email.",
            ) {
                done = false
                tried = false
            }
            return@Screen
        }
        Column(Modifier.fillMaxSize()) {
            KeyboardScrollView(
                modifier = Modifier.weight(1f),
                contentPadding = PaddingValues(start = 24.dp, end = 24.dp, top = 36.dp, bottom = 24.dp),
            ) {
                BasicText(
                    "Create your suiss Account",
                    modifier = Modifier
                        .fillMaxWidth()
                        .semantics { heading() },
                    style = Typography.xl3.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold, textAlign = TextAlign.Center),
                )
                Spacer(Modifier.height(10.dp))
                BasicText(
                    "One suiss Account is all you need to access every suiss service.",
                    modifier = Modifier.fillMaxWidth(),
                    style = Typography.lg.copy(color = colors.mutedForeground, textAlign = TextAlign.Center),
                )
                LinkText("Learn more ›", onLearnMore, Modifier.align(Alignment.CenterHorizontally))
                Legend("Your name")
                Row(horizontalArrangement = Arrangement.spacedBy(12.dp)) {
                    FieldStack(modifier = Modifier.weight(1f)) {
                        StackInput(first, { first = it }, "First name", accessory, 0)
                    }
                    FieldStack(modifier = Modifier.weight(1f)) {
                        StackInput(last, { last = it }, "Last name", accessory, 1)
                    }
                }
                Legend("Country or region and birthday")
                Picker(
                    value = country,
                    onValueChange = { country = it },
                    items = Countries,
                    modifier = Modifier.fillMaxWidth(),
                    placeholder = "Country or region",
                )
                Spacer(Modifier.height(8.dp))
                FieldStack {
                    StackInput(birthday, { birthday = it }, "Birthday (MM/DD/YYYY)", accessory, 2, keyboardType = KeyboardType.Number)
                }
                Legend("suiss Account")
                FieldStack {
                    StackInput(email, { email = it }, "name@example.com", accessory, 3, keyboardType = KeyboardType.Email)
                }
                BasicText(
                    "This will be your new suiss Account.",
                    modifier = Modifier.padding(top = 6.dp),
                    style = Typography.xs.copy(color = colors.mutedForeground),
                )
                Legend("Password")
                FieldStack(invalid = mismatch) {
                    StackInput(password, { password = it }, "Password", accessory, 4, keyboardType = KeyboardType.Password, secret = true)
                    StackDivider()
                    StackInput(
                        confirm,
                        { confirm = it },
                        "Confirm password",
                        accessory,
                        5,
                        keyboardType = KeyboardType.Password,
                        secret = true,
                        invalid = mismatch,
                        imeAction = ImeAction.Done,
                        onDone = ::submit,
                    )
                }
                AnimatedVisibility(
                    visible = password.isNotEmpty(),
                    enter = expandVertically(tween(400, easing = RevealEasing)),
                    exit = shrinkVertically(tween(400, easing = RevealEasing)),
                ) {
                    RuleList(password, Modifier.padding(start = 4.dp, end = 4.dp, top = 14.dp))
                }
                BasicText(
                    if (tried && !valid) "Please fill in every field correctly." else "",
                    modifier = Modifier
                        .fillMaxWidth()
                        .padding(top = 14.dp)
                        .heightIn(min = 20.dp)
                        .semantics { liveRegion = LiveRegionMode.Polite },
                    style = Typography.sm.copy(color = colors.destructive, textAlign = TextAlign.Center),
                )
            }
            BottomBar {
                BottomBarContent {
                    Button("Continue", ::submit, size = ButtonSize.Lg, block = true)
                }
            }
        }
        InputAccessoryBar(accessory, onDone = ::submit)
    }
}

@Composable
private fun Legend(text: String) {
    BasicText(
        text,
        modifier = Modifier.padding(top = 22.dp, bottom = 8.dp),
        style = Typography.sm.copy(color = EsuissTheme.colors.foreground, fontWeight = FontWeight.SemiBold),
    )
}

@Composable
private fun FieldStack(
    modifier: Modifier = Modifier,
    invalid: Boolean = false,
    content: @Composable ColumnScope.() -> Unit,
) {
    val colors = EsuissTheme.colors
    val shape = RoundedCornerShape(Radius.xl)
    val fill by animateColorAsState(
        if (invalid) colors.destructive.copy(alpha = if (colors.isDark) 0.1f else 0.05f) else colors.background,
        tween(200),
        label = "fill",
    )
    val edge by animateColorAsState(if (invalid) colors.destructive else colors.input, tween(200), label = "edge")
    Column(
        modifier = modifier
            .fillMaxWidth()
            .clip(shape)
            .background(fill, shape)
            .border(1.dp, edge, shape),
        content = content,
    )
}

@Composable
private fun StackDivider() {
    Box(
        Modifier
            .fillMaxWidth()
            .height(1.dp)
            .background(EsuissTheme.colors.input),
    )
}

@Composable
private fun StackInput(
    value: String,
    onValueChange: (String) -> Unit,
    placeholder: String,
    accessory: InputAccessoryState,
    index: Int,
    keyboardType: KeyboardType = KeyboardType.Text,
    secret: Boolean = false,
    invalid: Boolean = false,
    imeAction: ImeAction = ImeAction.Next,
    onDone: () -> Unit = {},
) {
    val colors = EsuissTheme.colors
    val focus = LocalFocusManager.current
    val style = Typography.lg.copy(color = colors.foreground)
    BasicTextField(
        value = value,
        onValueChange = onValueChange,
        modifier = Modifier
            .fillMaxWidth()
            .height(52.dp)
            .inputAccessory(accessory, index)
            .semantics {
                contentDescription = placeholder
                if (invalid) error("Passwords don't match")
            },
        textStyle = style,
        singleLine = true,
        cursorBrush = SolidColor(colors.primary),
        visualTransformation = if (secret) PasswordVisualTransformation() else VisualTransformation.None,
        keyboardOptions = KeyboardOptions(keyboardType = keyboardType, imeAction = imeAction),
        keyboardActions = KeyboardActions(
            onNext = { focus.moveFocus(FocusDirection.Next) },
            onDone = {
                focus.clearFocus()
                onDone()
            },
        ),
        decorationBox = { inner ->
            Box(Modifier.padding(horizontal = 16.dp), contentAlignment = Alignment.CenterStart) {
                if (value.isEmpty()) {
                    BasicText(placeholder, style = style.copy(color = colors.mutedForeground), maxLines = 1)
                }
                inner()
            }
        },
    )
}

@Composable
private fun RuleList(password: String, modifier: Modifier = Modifier) {
    Column(modifier, verticalArrangement = Arrangement.spacedBy(6.dp)) {
        PasswordRules.forEach { rule ->
            RuleRow(rule.label, rule.test(password))
        }
    }
}

@Composable
private fun RuleRow(label: String, met: Boolean) {
    val colors = EsuissTheme.colors
    val fill by animateColorAsState(if (met) Palette.green500 else Color.Transparent, tween(250), label = "fill")
    val edge by animateColorAsState(if (met) Color.Transparent else colors.border, tween(250), label = "edge")
    val scale by animateFloatAsState(if (met) 1f else 0f, tween(250, easing = PopEasing), label = "scale")
    Row(
        modifier = Modifier.semantics(mergeDescendants = true) { stateDescription = if (met) "Met" else "Not met" },
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        Box(
            Modifier
                .size(16.dp)
                .background(fill, CircleShape)
                .border(1.5.dp, edge, CircleShape),
            contentAlignment = Alignment.Center,
        ) {
            Icon(
                PhosphorIcons.Bold.Check,
                contentDescription = null,
                modifier = Modifier.graphicsLayer {
                    scaleX = scale
                    scaleY = scale
                },
                size = 10.dp,
                tint = colors.primaryForeground,
            )
        }
        BasicText(label, style = Typography.sm.copy(color = if (met) colors.foreground else colors.mutedForeground))
    }
}

@Composable
private fun LinkText(text: String, onClick: () -> Unit, modifier: Modifier = Modifier) {
    val interaction = remember { MutableInteractionSource() }
    val pressed by interaction.collectIsPressedAsState()
    BasicText(
        text,
        modifier = modifier
            .graphicsLayer { alpha = if (pressed) 0.8f else 1f }
            .clickable(interactionSource = interaction, indication = null, role = Role.Button, onClick = onClick)
            .padding(vertical = 6.dp),
        style = Typography.lg.copy(color = EsuissTheme.colors.primary, textAlign = TextAlign.Center),
    )
}

@Composable
private fun Success(title: String, description: String, onStartOver: () -> Unit) {
    val colors = EsuissTheme.colors
    val appear = remember { Animatable(0f) }
    LaunchedEffect(Unit) { appear.animateTo(1f, tween(1000, easing = RevealEasing)) }
    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(24.dp)
            .graphicsLayer {
                alpha = appear.value
                translationY = (1f - appear.value) * 18.dp.toPx()
            },
        verticalArrangement = Arrangement.spacedBy(14.dp, Alignment.CenterVertically),
        horizontalAlignment = Alignment.CenterHorizontally,
    ) {
        Box(
            Modifier
                .size(76.dp)
                .background(Palette.green500, CircleShape),
            contentAlignment = Alignment.Center,
        ) {
            Icon(PhosphorIcons.Bold.Check, contentDescription = null, size = 38.dp, tint = colors.primaryForeground)
        }
        BasicText(
            title,
            modifier = Modifier.semantics { heading() },
            style = Typography.xl3.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold, textAlign = TextAlign.Center),
        )
        BasicText(description, style = Typography.lg.copy(color = colors.mutedForeground, textAlign = TextAlign.Center))
        LinkText("Start over ›", onStartOver)
    }
}
```

Fields are grouped under short legends inside an `InputAccessoryBar` with a `KeyboardScrollView`, and the country uses a `Picker`. The password fields show a live checklist of password rules, and pressing Continue in the `BottomBar` validates every field, shows an error line if any fail, and swaps the form for a confirmation screen.

## Installation

**React Native**

```bash title="Terminal"
npx shadcn@latest add @esuiss-rn/signup-form
```

See [Installation](https://ui.suiss.app/docs/installation) for the `components.json` setup. Or use the suiss/ui CLI:

```bash title="Terminal"
npx @esuiss/ui@latest add signup-form
```

**SwiftUI**

```bash title="Terminal"
npx @esuiss/ui@latest add signup-form --platform swift
```

**Jetpack Compose**

```bash title="Terminal"
npx @esuiss/ui@latest add signup-form --platform kotlin
```

The CLI also adds what it builds on: [Bottom Bar](https://ui.suiss.app/docs/components/bottom-bar), [Button](https://ui.suiss.app/docs/components/button), [Icon](https://ui.suiss.app/docs/components/icon), [Input Accessory Bar](https://ui.suiss.app/docs/components/input-accessory-bar), [Keyboard Scroll View](https://ui.suiss.app/docs/components/keyboard-scroll-view), [Picker](https://ui.suiss.app/docs/patterns/picker), [Screen](https://ui.suiss.app/docs/components/screen), [Text](https://ui.suiss.app/docs/components/text). On React Native it also adds the animated view `components/ui/animated-view.tsx`.

### Manual

**React Native**

**Uniwind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install cn phosphor-react-native react-native-reanimated react-native-svg react-native-worklets
```

2. Add the pieces it builds on: [Bottom Bar](https://ui.suiss.app/docs/components/bottom-bar), [Button](https://ui.suiss.app/docs/components/button), [Icon](https://ui.suiss.app/docs/components/icon), [Input Accessory Bar](https://ui.suiss.app/docs/components/input-accessory-bar), [Keyboard Scroll View](https://ui.suiss.app/docs/components/keyboard-scroll-view), [Picker](https://ui.suiss.app/docs/patterns/picker), [Screen](https://ui.suiss.app/docs/components/screen), [Text](https://ui.suiss.app/docs/components/text), and the animated view `components/ui/animated-view.tsx` from the next step.

3. Copy the animated view into `components/ui/animated-view.tsx`:

- `uniwind/components/ui/animated-view.tsx`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/uniwind/components/ui/animated-view.tsx)

4. Copy the source into `components/blocks/signup-form.tsx`:

- `uniwind/components/blocks/signup-form.tsx`: shown above

5. Update the import paths to match your project.

**NativeWind**

1. Install the dependencies:

```bash title="Terminal"
npx expo install cn phosphor-react-native react-native-reanimated react-native-svg react-native-worklets
```

2. Add the pieces it builds on: [Bottom Bar](https://ui.suiss.app/docs/components/bottom-bar), [Button](https://ui.suiss.app/docs/components/button), [Icon](https://ui.suiss.app/docs/components/icon), [Input Accessory Bar](https://ui.suiss.app/docs/components/input-accessory-bar), [Keyboard Scroll View](https://ui.suiss.app/docs/components/keyboard-scroll-view), [Picker](https://ui.suiss.app/docs/patterns/picker), [Screen](https://ui.suiss.app/docs/components/screen), [Text](https://ui.suiss.app/docs/components/text), and the animated view `components/ui/animated-view.tsx` from the next step.

3. Copy the animated view into `components/ui/animated-view.tsx`:

- `nativewind/components/ui/animated-view.tsx`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/nativewind/components/ui/animated-view.tsx)

4. Copy the source into `components/blocks/signup-form.tsx`:

```tsx title="nativewind/components/blocks/signup-form.tsx"
import { cn } from "cn"
import { CheckIcon } from "phosphor-react-native"
import * as React from "react"
import { Pressable, View } from "react-native"
import {
  FadeIn,
  FadeInDown,
  ReduceMotion,
  ZoomIn,
} from "react-native-reanimated"
import { Picker, PickerItem } from "@/components/patterns/picker"
import { AnimatedView } from "@/components/ui/animated-view"
import { BottomBar, BottomBarContent } from "@/components/ui/bottom-bar"
import { Button } from "@/components/ui/button"
import { Icon } from "@/components/ui/icon"
import {
  InputAccessoryBar,
  InputAccessoryBarInput,
} from "@/components/ui/input-accessory-bar"
import { KeyboardScrollView } from "@/components/ui/keyboard-scroll-view"
import { Screen } from "@/components/ui/screen"
import { Text } from "@/components/ui/text"

const EMAIL = /.+@.+\..+/
const UPPERCASE = /[A-Z]/
const LOWERCASE = /[a-z]/
const DIGIT = /\d/
const SYMBOL = /[^A-Za-z0-9]/

const COUNTRIES = ["United States", "Germany", "Netherlands", "United Kingdom"]

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 STACK_CLASS =
  "overflow-hidden rounded-xl border border-input bg-background"

const INPUT_CLASS =
  "h-[3.25rem] rounded-none border-transparent bg-transparent px-4 text-lg"

const strength = (value: string) =>
  RULES.filter((rule) => rule.test(value)).length + (SYMBOL.test(value) ? 1 : 0)

function PasswordRules({ password }: { password: string }) {
  return (
    <View className="gap-1.5">
      {RULES.map((rule) => {
        const met = rule.test(password)
        return (
          <View
            key={rule.label}
            accessible
            accessibilityLabel={`${rule.label} ${met ? "(met)" : "(not met)"}`}
            className="flex-row items-center gap-2"
          >
            <View
              className={cn(
                "size-4 items-center justify-center rounded-full border-[1.5px]",
                met ? "border-transparent bg-green-500" : "border-border"
              )}
            >
              {met ? (
                <AnimatedView
                  entering={ZoomIn.duration(250).reduceMotion(
                    ReduceMotion.System
                  )}
                >
                  <Icon
                    as={CheckIcon}
                    weight="bold"
                    className="size-2.5 text-primary-foreground"
                  />
                </AnimatedView>
              ) : null}
            </View>
            <Text
              className={cn(
                "text-sm",
                met ? "text-foreground" : "text-muted-foreground"
              )}
            >
              {rule.label}
            </Text>
          </View>
        )
      })}
    </View>
  )
}

function Legend({ children }: { children: React.ReactNode }) {
  return (
    <Text role="heading" className="mb-2 text-sm font-semibold">
      {children}
    </Text>
  )
}

type SignupFormValues = {
  firstName: string
  lastName: string
  country: string
  birthday: string
  email: string
  password: string
}

function SignupForm({
  onLearnMore,
  onSubmit,
}: {
  onLearnMore?: () => void
  onSubmit?: (values: SignupFormValues) => void
}) {
  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) => (value: string) =>
    setForm((current) => ({ ...current, [key]: value }))

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

  const submit = () => {
    setTried(true)
    if (!valid) return
    onSubmit?.({
      firstName: form.first,
      lastName: form.last,
      country: form.country,
      birthday: form.birthday,
      email: form.email,
      password: form.password,
    })
    setDone(true)
  }

  if (done) {
    return (
      <Screen>
        <View className="flex-1 items-center justify-center p-6">
          <AnimatedView
            entering={FadeInDown.duration(600).reduceMotion(
              ReduceMotion.System
            )}
            className="items-center gap-3.5"
          >
            <View className="size-[4.75rem] items-center justify-center rounded-full bg-green-500">
              <Icon
                as={CheckIcon}
                weight="bold"
                className="size-[2.375rem] text-primary-foreground"
              />
            </View>
            <Text role="heading" className="text-center text-3xl font-semibold">
              Your suiss Account is ready.
            </Text>
            <Text className="text-center text-lg text-muted-foreground">
              We sent a verification code to {form.email}.
            </Text>
            <Pressable
              role="link"
              hitSlop={8}
              onPress={() => {
                setDone(false)
                setTried(false)
              }}
              className="active:opacity-60"
            >
              <Text className="text-lg text-primary">Start over ›</Text>
            </Pressable>
          </AnimatedView>
        </View>
      </Screen>
    )
  }

  return (
    <Screen>
      <InputAccessoryBar>
        <KeyboardScrollView contentContainerClassName="px-6 pt-9 pb-6">
          <Text role="heading" className="text-center text-3xl font-semibold">
            Create your suiss Account
          </Text>
          <Text className="mt-2.5 text-center text-lg text-muted-foreground">
            One suiss Account is all you need to access every suiss service.{" "}
            <Text
              role="link"
              className="text-lg text-primary"
              onPress={onLearnMore}
            >
              Learn more ›
            </Text>
          </Text>
          <View className="mt-[1.375rem]">
            <Legend>Your name</Legend>
            <View className="flex-row gap-3">
              <View className={cn(STACK_CLASS, "flex-1")}>
                <InputAccessoryBarInput
                  value={form.first}
                  onChangeText={update("first")}
                  placeholder="First name"
                  accessibilityLabel="First name"
                  autoComplete="given-name"
                  textContentType="givenName"
                  className={INPUT_CLASS}
                />
              </View>
              <View className={cn(STACK_CLASS, "flex-1")}>
                <InputAccessoryBarInput
                  value={form.last}
                  onChangeText={update("last")}
                  placeholder="Last name"
                  accessibilityLabel="Last name"
                  autoComplete="family-name"
                  textContentType="familyName"
                  className={INPUT_CLASS}
                />
              </View>
            </View>
          </View>
          <View className="mt-[1.375rem]">
            <Legend>Country or region and birthday</Legend>
            <View className={STACK_CLASS}>
              <Picker
                value={form.country}
                onValueChange={update("country")}
                accessibilityLabel="Country or region"
                className="h-[3.25rem] w-full self-stretch rounded-none border-transparent bg-transparent ps-4"
              >
                {COUNTRIES.map((country) => (
                  <PickerItem key={country} value={country}>
                    {country}
                  </PickerItem>
                ))}
              </Picker>
              <View className="h-px bg-input" />
              <InputAccessoryBarInput
                value={form.birthday}
                onChangeText={update("birthday")}
                placeholder="Birthday (MM/DD/YYYY)"
                accessibilityLabel="Birthday"
                autoComplete="birthdate-full"
                keyboardType="numbers-and-punctuation"
                className={INPUT_CLASS}
              />
            </View>
          </View>
          <View className="mt-[1.375rem]">
            <Legend>suiss Account</Legend>
            <View className={STACK_CLASS}>
              <InputAccessoryBarInput
                value={form.email}
                onChangeText={update("email")}
                placeholder="name@example.com"
                accessibilityLabel="Email"
                accessibilityHint="This will be your new suiss Account."
                autoComplete="email"
                textContentType="emailAddress"
                keyboardType="email-address"
                autoCapitalize="none"
                autoCorrect={false}
                className={INPUT_CLASS}
              />
            </View>
            <Text className="mt-1.5 text-xs text-muted-foreground">
              This will be your new suiss Account.
            </Text>
          </View>
          <View className="mt-[1.375rem]">
            <Legend>Password</Legend>
            <View
              className={cn(
                STACK_CLASS,
                mismatch &&
                  "border-destructive bg-destructive-5 dark:bg-destructive-10"
              )}
            >
              <InputAccessoryBarInput
                value={form.password}
                onChangeText={update("password")}
                placeholder="Password"
                accessibilityLabel="Password"
                autoComplete="new-password"
                textContentType="newPassword"
                secureTextEntry
                className={INPUT_CLASS}
              />
              <View
                className={cn("h-px", mismatch ? "bg-destructive" : "bg-input")}
              />
              <InputAccessoryBarInput
                value={form.confirm}
                onChangeText={update("confirm")}
                placeholder="Confirm password"
                accessibilityLabel="Confirm password"
                autoComplete="new-password"
                textContentType="newPassword"
                secureTextEntry
                className={INPUT_CLASS}
              />
            </View>
            {form.password ? (
              <AnimatedView
                entering={FadeIn.duration(300).reduceMotion(
                  ReduceMotion.System
                )}
                className="px-1 pt-3.5"
              >
                <PasswordRules password={form.password} />
              </AnimatedView>
            ) : null}
          </View>
          <Text
            accessibilityLiveRegion="polite"
            className="mt-3.5 min-h-5 text-center text-sm text-destructive"
          >
            {tried && !valid ? "Please fill in every field correctly." : ""}
          </Text>
        </KeyboardScrollView>
      </InputAccessoryBar>
      <BottomBar separator>
        <BottomBarContent>
          <Button size="lg" block onPress={submit}>
            <Text>Continue</Text>
          </Button>
        </BottomBarContent>
      </BottomBar>
    </Screen>
  )
}

export type { SignupFormValues }
export { SignupForm }
```

5. Update the import paths to match your project.

**StyleSheet**

1. Install the dependencies:

```bash title="Terminal"
npx expo install phosphor-react-native react-native-reanimated react-native-svg react-native-worklets
```

2. Add the pieces it builds on: [Bottom Bar](https://ui.suiss.app/docs/components/bottom-bar), [Button](https://ui.suiss.app/docs/components/button), [Icon](https://ui.suiss.app/docs/components/icon), [Input Accessory Bar](https://ui.suiss.app/docs/components/input-accessory-bar), [Keyboard Scroll View](https://ui.suiss.app/docs/components/keyboard-scroll-view), [Picker](https://ui.suiss.app/docs/patterns/picker), [Screen](https://ui.suiss.app/docs/components/screen), [Text](https://ui.suiss.app/docs/components/text), and the animated view `components/ui/animated-view.tsx` from the next step.

3. Copy the animated view into `components/ui/animated-view.tsx`:

- `stylesheet/components/ui/animated-view.tsx`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/stylesheet/components/ui/animated-view.tsx)

4. Copy the source into `components/blocks/signup-form.tsx`:

```tsx title="stylesheet/components/blocks/signup-form.tsx"
import { CheckIcon } from "phosphor-react-native"
import * as React from "react"
import { Pressable, StyleSheet, View } from "react-native"
import {
  FadeIn,
  FadeInDown,
  ReduceMotion,
  ZoomIn,
} from "react-native-reanimated"
import { Picker, PickerItem } from "@/components/patterns/picker"
import { AnimatedView } from "@/components/ui/animated-view"
import { BottomBar, BottomBarContent } from "@/components/ui/bottom-bar"
import { Button } from "@/components/ui/button"
import { Icon } from "@/components/ui/icon"
import {
  InputAccessoryBar,
  InputAccessoryBarInput,
} from "@/components/ui/input-accessory-bar"
import { KeyboardScrollView } from "@/components/ui/keyboard-scroll-view"
import { Screen } from "@/components/ui/screen"
import { Text } from "@/components/ui/text"
import { fontSize, palette, radius, useTheme } from "@/components/ui/theme"

const EMAIL = /.+@.+\..+/
const UPPERCASE = /[A-Z]/
const LOWERCASE = /[a-z]/
const DIGIT = /\d/
const SYMBOL = /[^A-Za-z0-9]/

const COUNTRIES = ["United States", "Germany", "Netherlands", "United Kingdom"]

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) },
]

function withAlpha(color: string, alpha: number) {
  if (!color.startsWith("#") || color.length !== 7) return color
  const r = Number.parseInt(color.slice(1, 3), 16)
  const g = Number.parseInt(color.slice(3, 5), 16)
  const b = Number.parseInt(color.slice(5, 7), 16)
  return `rgba(${r}, ${g}, ${b}, ${alpha})`
}

const styles = StyleSheet.create({
  stack: {
    overflow: "hidden",
    borderRadius: radius.xl,
    borderWidth: 1,
  },
  input: {
    ...fontSize.lg,
    height: 52,
    borderRadius: 0,
    borderColor: "transparent",
    backgroundColor: "transparent",
    paddingHorizontal: 16,
  },
  picker: {
    height: 52,
    width: "100%",
    alignSelf: "stretch",
    borderRadius: 0,
    borderColor: "transparent",
    backgroundColor: "transparent",
    paddingStart: 16,
  },
  rules: { gap: 6 },
  rule: { flexDirection: "row", alignItems: "center", gap: 8 },
  ruleMark: {
    width: 16,
    height: 16,
    alignItems: "center",
    justifyContent: "center",
    borderRadius: radius.full,
    borderWidth: 1.5,
  },
  center: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center",
    padding: 24,
  },
  done: { alignItems: "center", gap: 14 },
  badge: {
    width: 76,
    height: 76,
    alignItems: "center",
    justifyContent: "center",
    borderRadius: radius.full,
  },
  heading: { ...fontSize["3xl"], textAlign: "center", fontWeight: "600" },
  scroll: { paddingHorizontal: 24, paddingTop: 36, paddingBottom: 24 },
  section: { marginTop: 22 },
  row: { flexDirection: "row", gap: 12 },
  legend: { ...fontSize.sm, marginBottom: 8, fontWeight: "600" },
})

function PasswordRules({ password }: { password: string }) {
  const { colors } = useTheme()
  return (
    <View style={styles.rules}>
      {RULES.map((rule) => {
        const met = rule.test(password)
        return (
          <View
            key={rule.label}
            accessible
            accessibilityLabel={`${rule.label} ${met ? "(met)" : "(not met)"}`}
            style={styles.rule}
          >
            <View
              style={[
                styles.ruleMark,
                met
                  ? {
                      borderColor: "transparent",
                      backgroundColor: palette.green500,
                    }
                  : { borderColor: colors.border },
              ]}
            >
              {met ? (
                <AnimatedView
                  entering={ZoomIn.duration(250).reduceMotion(
                    ReduceMotion.System
                  )}
                >
                  <Icon
                    as={CheckIcon}
                    weight="bold"
                    size={10}
                    color={colors.primaryForeground}
                  />
                </AnimatedView>
              ) : null}
            </View>
            <Text
              style={{
                ...fontSize.sm,
                color: met ? colors.foreground : colors.mutedForeground,
              }}
            >
              {rule.label}
            </Text>
          </View>
        )
      })}
    </View>
  )
}

function Legend({ children }: { children: React.ReactNode }) {
  return (
    <Text role="heading" style={styles.legend}>
      {children}
    </Text>
  )
}

const strength = (value: string) =>
  RULES.filter((rule) => rule.test(value)).length + (SYMBOL.test(value) ? 1 : 0)

type SignupFormValues = {
  firstName: string
  lastName: string
  country: string
  birthday: string
  email: string
  password: string
}

function SignupForm({
  onLearnMore,
  onSubmit,
}: {
  onLearnMore?: () => void
  onSubmit?: (values: SignupFormValues) => void
}) {
  const { colors, isDark } = useTheme()
  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) => (value: string) =>
    setForm((current) => ({ ...current, [key]: value }))

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

  const submit = () => {
    setTried(true)
    if (!valid) return
    onSubmit?.({
      firstName: form.first,
      lastName: form.last,
      country: form.country,
      birthday: form.birthday,
      email: form.email,
      password: form.password,
    })
    setDone(true)
  }

  const stack = [
    styles.stack,
    { borderColor: colors.input, backgroundColor: colors.background },
  ]
  const divider = { height: 1, backgroundColor: colors.input }

  if (done) {
    return (
      <Screen>
        <View style={styles.center}>
          <AnimatedView
            entering={FadeInDown.duration(600).reduceMotion(
              ReduceMotion.System
            )}
            style={styles.done}
          >
            <View style={[styles.badge, { backgroundColor: palette.green500 }]}>
              <Icon
                as={CheckIcon}
                weight="bold"
                size={38}
                color={colors.primaryForeground}
              />
            </View>
            <Text role="heading" style={styles.heading}>
              Your suiss Account is ready.
            </Text>
            <Text
              style={{
                ...fontSize.lg,
                textAlign: "center",
                color: colors.mutedForeground,
              }}
            >
              We sent a verification code to {form.email}.
            </Text>
            <Pressable
              role="link"
              hitSlop={8}
              onPress={() => {
                setDone(false)
                setTried(false)
              }}
              style={({ pressed }) => pressed && { opacity: 0.6 }}
            >
              <Text style={{ ...fontSize.lg, color: colors.primary }}>
                Start over ›
              </Text>
            </Pressable>
          </AnimatedView>
        </View>
      </Screen>
    )
  }

  return (
    <Screen>
      <InputAccessoryBar>
        <KeyboardScrollView contentContainerStyle={styles.scroll}>
          <Text role="heading" style={styles.heading}>
            Create your suiss Account
          </Text>
          <Text
            style={{
              ...fontSize.lg,
              marginTop: 10,
              textAlign: "center",
              color: colors.mutedForeground,
            }}
          >
            One suiss Account is all you need to access every suiss service.{" "}
            <Text
              role="link"
              style={{ ...fontSize.lg, color: colors.primary }}
              onPress={onLearnMore}
            >
              Learn more ›
            </Text>
          </Text>
          <View style={styles.section}>
            <Legend>Your name</Legend>
            <View style={styles.row}>
              <View style={[stack, { flex: 1 }]}>
                <InputAccessoryBarInput
                  value={form.first}
                  onChangeText={update("first")}
                  placeholder="First name"
                  accessibilityLabel="First name"
                  autoComplete="given-name"
                  textContentType="givenName"
                  style={styles.input}
                />
              </View>
              <View style={[stack, { flex: 1 }]}>
                <InputAccessoryBarInput
                  value={form.last}
                  onChangeText={update("last")}
                  placeholder="Last name"
                  accessibilityLabel="Last name"
                  autoComplete="family-name"
                  textContentType="familyName"
                  style={styles.input}
                />
              </View>
            </View>
          </View>
          <View style={styles.section}>
            <Legend>Country or region and birthday</Legend>
            <View style={stack}>
              <Picker
                value={form.country}
                onValueChange={update("country")}
                accessibilityLabel="Country or region"
                style={styles.picker}
              >
                {COUNTRIES.map((country) => (
                  <PickerItem key={country} value={country}>
                    {country}
                  </PickerItem>
                ))}
              </Picker>
              <View style={divider} />
              <InputAccessoryBarInput
                value={form.birthday}
                onChangeText={update("birthday")}
                placeholder="Birthday (MM/DD/YYYY)"
                accessibilityLabel="Birthday"
                autoComplete="birthdate-full"
                keyboardType="numbers-and-punctuation"
                style={styles.input}
              />
            </View>
          </View>
          <View style={styles.section}>
            <Legend>suiss Account</Legend>
            <View style={stack}>
              <InputAccessoryBarInput
                value={form.email}
                onChangeText={update("email")}
                placeholder="name@example.com"
                accessibilityLabel="Email"
                accessibilityHint="This will be your new suiss Account."
                autoComplete="email"
                textContentType="emailAddress"
                keyboardType="email-address"
                autoCapitalize="none"
                autoCorrect={false}
                style={styles.input}
              />
            </View>
            <Text
              style={{
                ...fontSize.xs,
                marginTop: 6,
                color: colors.mutedForeground,
              }}
            >
              This will be your new suiss Account.
            </Text>
          </View>
          <View style={styles.section}>
            <Legend>Password</Legend>
            <View
              style={[
                stack,
                mismatch && {
                  borderColor: colors.destructive,
                  backgroundColor: withAlpha(
                    colors.destructive,
                    isDark ? 0.1 : 0.05
                  ),
                },
              ]}
            >
              <InputAccessoryBarInput
                value={form.password}
                onChangeText={update("password")}
                placeholder="Password"
                accessibilityLabel="Password"
                autoComplete="new-password"
                textContentType="newPassword"
                secureTextEntry
                style={styles.input}
              />
              <View
                style={[
                  divider,
                  mismatch && { backgroundColor: colors.destructive },
                ]}
              />
              <InputAccessoryBarInput
                value={form.confirm}
                onChangeText={update("confirm")}
                placeholder="Confirm password"
                accessibilityLabel="Confirm password"
                autoComplete="new-password"
                textContentType="newPassword"
                secureTextEntry
                style={styles.input}
              />
            </View>
            {form.password ? (
              <AnimatedView
                entering={FadeIn.duration(300).reduceMotion(
                  ReduceMotion.System
                )}
                style={{ paddingHorizontal: 4, paddingTop: 14 }}
              >
                <PasswordRules password={form.password} />
              </AnimatedView>
            ) : null}
          </View>
          <Text
            accessibilityLiveRegion="polite"
            style={{
              ...fontSize.sm,
              marginTop: 14,
              minHeight: 20,
              textAlign: "center",
              color: colors.destructive,
            }}
          >
            {tried && !valid ? "Please fill in every field correctly." : ""}
          </Text>
        </KeyboardScrollView>
      </InputAccessoryBar>
      <BottomBar separator>
        <BottomBarContent>
          <Button size="lg" block onPress={submit}>
            <Text>Continue</Text>
          </Button>
        </BottomBarContent>
      </BottomBar>
    </Screen>
  )
}

export type { SignupFormValues }
export { SignupForm }
```

5. Update the import paths to match your project.

**SwiftUI**

1. Target iOS 17 or later.

2. Add the Swift package to your app:

```swift title="Package.swift"
.package(url: "https://github.com/phosphor-icons/swift", from: "2.1.0")
```

3. Copy the theme files every view uses:

- `swift/Theme/Theme.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Theme/Theme.swift)

- `swift/Theme/Tokens.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Theme/Tokens.swift)

4. Copy the source and every file it uses:

- `swift/Blocks/SignupForm.swift`: shown above

- `swift/Components/AlertDialog.swift`: see [Alert Dialog](https://ui.suiss.app/docs/components/alert-dialog.md)

- `swift/Components/AlertSheet.swift`: see [Alert Sheet](https://ui.suiss.app/docs/components/alert-sheet.md)

- `swift/Components/BottomBar.swift`: see [Bottom Bar](https://ui.suiss.app/docs/components/bottom-bar.md)

- `swift/Components/Button.swift`: see [Button](https://ui.suiss.app/docs/components/button.md)

- `swift/Components/ButtonGroup.swift`: see [Button Group](https://ui.suiss.app/docs/components/button-group.md)

- `swift/Components/Checkbox.swift`: see [Checkbox](https://ui.suiss.app/docs/components/checkbox.md)

- `swift/Components/Dialog.swift`: see [Dialog](https://ui.suiss.app/docs/components/dialog.md)

- `swift/Components/DropdownMenu.swift`: see [Dropdown Menu](https://ui.suiss.app/docs/components/dropdown-menu.md)

- `swift/Components/Field.swift`: see [Field](https://ui.suiss.app/docs/components/field.md)

- `swift/Components/Haptics.swift`: see [Haptics](https://ui.suiss.app/docs/components/haptics.md)

- `swift/Components/Input.swift`: see [Input](https://ui.suiss.app/docs/components/input.md)

- `swift/Components/InputAccessoryBar.swift`: see [Input Accessory Bar](https://ui.suiss.app/docs/components/input-accessory-bar.md)

- `swift/Components/KeyboardScrollView.swift`: see [Keyboard Scroll View](https://ui.suiss.app/docs/components/keyboard-scroll-view.md)

- `swift/Components/Overlay.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Components/Overlay.swift)

- `swift/Components/Screen.swift`: see [Screen](https://ui.suiss.app/docs/components/screen.md)

- `swift/Components/Select.swift`: see [Select](https://ui.suiss.app/docs/components/select.md)

- `swift/Components/Separator.swift`: see [Separator](https://ui.suiss.app/docs/components/separator.md)

- `swift/Components/TextVariant.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Components/TextVariant.swift)

- `swift/Patterns/Adaptive.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Patterns/Adaptive.swift)

- `swift/Patterns/Confirm.swift`: see [Confirm](https://ui.suiss.app/docs/patterns/confirm.md)

- `swift/Patterns/OptionPicker.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Patterns/OptionPicker.swift)

5. Add the files to your app target.

**Jetpack Compose**

1. Set `minSdk` to 24 or higher and add the dependencies:

```kotlin title="build.gradle.kts"
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")
implementation("com.adamglin:phosphor-icon-android:1.0.0")
```

2. Copy the theme files every view uses:

- `kotlin/theme/Theme.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/theme/Theme.kt)

- `kotlin/theme/Tokens.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/theme/Tokens.kt)

3. Copy the source and every file it uses:

- `kotlin/blocks/SignupForm.kt`: shown above

- `kotlin/components/BottomBar.kt`: see [Bottom Bar](https://ui.suiss.app/docs/components/bottom-bar.md)

- `kotlin/components/Button.kt`: see [Button](https://ui.suiss.app/docs/components/button.md)

- `kotlin/components/ButtonGroup.kt`: see [Button Group](https://ui.suiss.app/docs/components/button-group.md)

- `kotlin/components/Content.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/components/Content.kt)

- `kotlin/components/Field.kt`: see [Field](https://ui.suiss.app/docs/components/field.md)

- `kotlin/components/FormSupport.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/components/FormSupport.kt)

- `kotlin/components/Icon.kt`: see [Icon](https://ui.suiss.app/docs/components/icon.md)

- `kotlin/components/InputAccessoryBar.kt`: see [Input Accessory Bar](https://ui.suiss.app/docs/components/input-accessory-bar.md)

- `kotlin/components/KeyboardScrollView.kt`: see [Keyboard Scroll View](https://ui.suiss.app/docs/components/keyboard-scroll-view.md)

- `kotlin/components/Label.kt`: see [Label](https://ui.suiss.app/docs/components/label.md)

- `kotlin/components/Overlay.kt`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/kotlin/ui/src/main/kotlin/com/esuiss/ui/components/Overlay.kt)

- `kotlin/components/Screen.kt`: see [Screen](https://ui.suiss.app/docs/components/screen.md)

- `kotlin/components/Select.kt`: see [Select](https://ui.suiss.app/docs/components/select.md)

- `kotlin/components/Separator.kt`: see [Separator](https://ui.suiss.app/docs/components/separator.md)

- `kotlin/patterns/Picker.kt`: see [Picker](https://ui.suiss.app/docs/patterns/picker.md)

4. Update the `com.esuiss.ui` package names to match your project.

## Usage

```tsx
import { SignupForm } from "@/components/blocks/signup-form"
```

```tsx
<SignupForm />
```

Replace `COUNTRIES` and `RULES` to match your requirements. `onSubmit` receives the form values (typed as `SignupFormValues`), and `onLearnMore` handles the info link.

