# Login Account

A two-step account sign-in form that asks for an email, then reveals a password field.

Login Account 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/login-account

**React**

**Example**

```tsx title="components/blocks/login-account.tsx"
"use client"

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

import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Spinner } from "@/components/ui/spinner"

const EMAIL = /.+@.+\..+/

const SHAKE = [0, -10, 9, -6, 4, 0].map((x) => ({
  transform: `translateX(${x}px)`,
}))

function SubmitArrow({ ready, busy }: { ready: boolean; busy: boolean }) {
  return (
    <Button
      type="submit"
      variant="outline"
      size="icon-sm"
      aria-label="Continue"
      disabled={!ready || busy}
      className="me-2.5 size-7.5 border-[1.5px] border-muted-foreground bg-transparent text-muted-foreground dark:bg-transparent disabled:opacity-50"
    >
      {busy ? (
        <Spinner />
      ) : (
        <ArrowRightIcon className="size-3.5 rtl:rotate-180" />
      )}
    </Button>
  )
}

export function LoginAccount() {
  const idPrefix = React.useId()
  const [step, setStep] = React.useState<"email" | "password">("email")
  const [email, setEmail] = React.useState("")
  const [password, setPassword] = React.useState("")
  const [error, setError] = React.useState("")
  const [busy, setBusy] = React.useState(false)
  const [signedIn, setSignedIn] = React.useState(false)
  const fieldsRef = React.useRef<HTMLDivElement>(null)
  const passwordRef = React.useRef<HTMLInputElement>(null)

  const fail = (message: string) => {
    setError(message)
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return
    fieldsRef.current?.animate(SHAKE, { duration: 420, easing: "ease-out" })
  }

  const submit = (event: React.FormEvent) => {
    event.preventDefault()
    if (step === "email") {
      if (!EMAIL.test(email)) return fail("Enter a valid suiss Account.")
      setError("")
      setStep("password")
      window.setTimeout(() => passwordRef.current?.focus(), 380)
      return
    }
    if (password.length < 4)
      return fail("Your suiss Account or password was incorrect.")
    setError("")
    setBusy(true)
    window.setTimeout(() => {
      setBusy(false)
      setSignedIn(true)
    }, 1200)
  }

  if (signedIn) {
    return (
      <section className="flex min-h-150 items-center justify-center p-6">
        <div className="flex 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">
          <Avatar className="size-20">
            <AvatarFallback className="bg-none bg-muted text-3xl text-foreground">
              {email[0]}
            </AvatarFallback>
          </Avatar>
          <h1 className="text-5xl font-semibold tracking-tight">Hello.</h1>
          <p className="text-lg text-muted-foreground">{email}</p>
          <button
            type="button"
            onClick={() => {
              setSignedIn(false)
              setStep("email")
              setPassword("")
            }}
            className="rounded-xs text-lg text-primary outline-none hover:underline focus-visible:ring-3 focus-visible:ring-ring/50"
          >
            Sign out ›
          </button>
        </div>
      </section>
    )
  }

  return (
    <section className="flex min-h-150 items-center justify-center p-6">
      <form
        noValidate
        onSubmit={submit}
        className="flex w-115 max-w-full flex-col items-center text-center"
      >
        <h1 className="mb-8 text-4xl font-semibold tracking-tight text-balance md:text-5xl">
          Sign in with your suiss Account
        </h1>
        <div
          ref={fieldsRef}
          data-invalid={error ? "" : undefined}
          className="w-full overflow-hidden rounded-xl border border-input bg-background text-start 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"
        >
          <div className="flex items-center">
            <Input
              type="email"
              value={email}
              onChange={(event) => {
                setEmail(event.target.value)
                setError("")
                if (step === "password") setStep("email")
              }}
              placeholder="Email or phone number"
              aria-label="Email or phone number"
              aria-invalid={(step === "email" && !!error) || undefined}
              aria-describedby={`${idPrefix}-error`}
              autoComplete="username"
              className="h-14 rounded-none bg-transparent px-4 text-lg focus-visible:border-transparent focus-visible:ring-0 aria-invalid:ring-0 aria-invalid:border-transparent aria-invalid:bg-transparent dark:aria-invalid:bg-transparent"
            />
            {step === "email" && <SubmitArrow ready={!!email} busy={busy} />}
          </div>
          <div
            data-open={step === "password" ? "" : undefined}
            className="grid grid-rows-[0fr] transition-[grid-template-rows] duration-450 ease-[cubic-bezier(0.32,0.72,0,1)] data-open:grid-rows-[1fr] motion-reduce:transition-none"
          >
            <div
              inert={step !== "password"}
              className="min-h-0 overflow-hidden"
            >
              <div className="flex items-center border-t border-input">
                <Input
                  ref={passwordRef}
                  type="password"
                  value={password}
                  onChange={(event) => {
                    setPassword(event.target.value)
                    setError("")
                  }}
                  placeholder="Password"
                  aria-label="Password"
                  aria-invalid={(step === "password" && !!error) || undefined}
                  aria-describedby={`${idPrefix}-error`}
                  autoComplete="current-password"
                  tabIndex={step === "password" ? 0 : -1}
                  className="h-14 rounded-none bg-transparent px-4 text-lg focus-visible:border-transparent focus-visible:ring-0 aria-invalid:ring-0 aria-invalid:border-transparent aria-invalid:bg-transparent dark:aria-invalid:bg-transparent"
                />
                <SubmitArrow ready={!!password} busy={busy} />
              </div>
            </div>
          </div>
        </div>
        <p
          id={`${idPrefix}-error`}
          aria-live="polite"
          className="mt-2.5 min-h-5.5 text-sm text-destructive"
        >
          {error}
        </p>
        <Label className="mt-1.5 cursor-pointer gap-2 text-sm font-normal">
          <Checkbox defaultChecked />
          Remember me
        </Label>
        <div className="mt-10 flex flex-col items-center gap-2.5 text-sm">
          <a href="#forgot" className="text-primary hover:underline">
            Forgot password? ›
          </a>
          <p className="flex flex-wrap justify-center gap-x-1 text-muted-foreground">
            <span>Don't have a suiss Account?</span>
            <a href="#create" className="text-primary hover:underline">
              Create yours now ›
            </a>
          </p>
        </div>
      </form>
    </section>
  )
}
```

A sign-in form that asks for an email first, then reveals a password field below it. Errors appear inline and shake the fields, followed by a Remember me checkbox and links for a forgotten password and account creation. A successful sign-in shows a greeting with a Sign out link. Use it for any email-and-password login.

## Installation

### CLI

```bash title="Terminal"
npx shadcn@latest add @esuiss/login-account
```

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 login-account
```

### Manual

1. Install the dependencies:

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

2. Add the pieces it builds on: [Avatar](https://ui.suiss.app/docs/components/avatar), [Button](https://ui.suiss.app/docs/components/button), [Checkbox](https://ui.suiss.app/docs/components/checkbox), [Input](https://ui.suiss.app/docs/components/input), [Label](https://ui.suiss.app/docs/components/label), [Spinner](https://ui.suiss.app/docs/components/spinner).

3. Copy the source into `components/blocks/login-account.tsx`:

```tsx title="components/blocks/login-account.tsx"
"use client"

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

import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Spinner } from "@/components/ui/spinner"

const EMAIL = /.+@.+\..+/

const SHAKE = [0, -10, 9, -6, 4, 0].map((x) => ({
  transform: `translateX(${x}px)`,
}))

function SubmitArrow({ ready, busy }: { ready: boolean; busy: boolean }) {
  return (
    <Button
      type="submit"
      variant="outline"
      size="icon-sm"
      aria-label="Continue"
      disabled={!ready || busy}
      className="me-2.5 size-7.5 border-[1.5px] border-muted-foreground bg-transparent text-muted-foreground dark:bg-transparent disabled:opacity-50"
    >
      {busy ? (
        <Spinner />
      ) : (
        <ArrowRightIcon className="size-3.5 rtl:rotate-180" />
      )}
    </Button>
  )
}

export function LoginAccount() {
  const idPrefix = React.useId()
  const [step, setStep] = React.useState<"email" | "password">("email")
  const [email, setEmail] = React.useState("")
  const [password, setPassword] = React.useState("")
  const [error, setError] = React.useState("")
  const [busy, setBusy] = React.useState(false)
  const [signedIn, setSignedIn] = React.useState(false)
  const fieldsRef = React.useRef<HTMLDivElement>(null)
  const passwordRef = React.useRef<HTMLInputElement>(null)

  const fail = (message: string) => {
    setError(message)
    if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return
    fieldsRef.current?.animate(SHAKE, { duration: 420, easing: "ease-out" })
  }

  const submit = (event: React.FormEvent) => {
    event.preventDefault()
    if (step === "email") {
      if (!EMAIL.test(email)) return fail("Enter a valid suiss Account.")
      setError("")
      setStep("password")
      window.setTimeout(() => passwordRef.current?.focus(), 380)
      return
    }
    if (password.length < 4)
      return fail("Your suiss Account or password was incorrect.")
    setError("")
    setBusy(true)
    window.setTimeout(() => {
      setBusy(false)
      setSignedIn(true)
    }, 1200)
  }

  if (signedIn) {
    return (
      <section className="flex min-h-150 items-center justify-center p-6">
        <div className="flex 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">
          <Avatar className="size-20">
            <AvatarFallback className="bg-none bg-muted text-3xl text-foreground">
              {email[0]}
            </AvatarFallback>
          </Avatar>
          <h1 className="text-5xl font-semibold tracking-tight">Hello.</h1>
          <p className="text-lg text-muted-foreground">{email}</p>
          <button
            type="button"
            onClick={() => {
              setSignedIn(false)
              setStep("email")
              setPassword("")
            }}
            className="rounded-xs text-lg text-primary outline-none hover:underline focus-visible:ring-3 focus-visible:ring-ring/50"
          >
            Sign out ›
          </button>
        </div>
      </section>
    )
  }

  return (
    <section className="flex min-h-150 items-center justify-center p-6">
      <form
        noValidate
        onSubmit={submit}
        className="flex w-115 max-w-full flex-col items-center text-center"
      >
        <h1 className="mb-8 text-4xl font-semibold tracking-tight text-balance md:text-5xl">
          Sign in with your suiss Account
        </h1>
        <div
          ref={fieldsRef}
          data-invalid={error ? "" : undefined}
          className="w-full overflow-hidden rounded-xl border border-input bg-background text-start 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"
        >
          <div className="flex items-center">
            <Input
              type="email"
              value={email}
              onChange={(event) => {
                setEmail(event.target.value)
                setError("")
                if (step === "password") setStep("email")
              }}
              placeholder="Email or phone number"
              aria-label="Email or phone number"
              aria-invalid={(step === "email" && !!error) || undefined}
              aria-describedby={`${idPrefix}-error`}
              autoComplete="username"
              className="h-14 rounded-none bg-transparent px-4 text-lg focus-visible:border-transparent focus-visible:ring-0 aria-invalid:ring-0 aria-invalid:border-transparent aria-invalid:bg-transparent dark:aria-invalid:bg-transparent"
            />
            {step === "email" && <SubmitArrow ready={!!email} busy={busy} />}
          </div>
          <div
            data-open={step === "password" ? "" : undefined}
            className="grid grid-rows-[0fr] transition-[grid-template-rows] duration-450 ease-[cubic-bezier(0.32,0.72,0,1)] data-open:grid-rows-[1fr] motion-reduce:transition-none"
          >
            <div
              inert={step !== "password"}
              className="min-h-0 overflow-hidden"
            >
              <div className="flex items-center border-t border-input">
                <Input
                  ref={passwordRef}
                  type="password"
                  value={password}
                  onChange={(event) => {
                    setPassword(event.target.value)
                    setError("")
                  }}
                  placeholder="Password"
                  aria-label="Password"
                  aria-invalid={(step === "password" && !!error) || undefined}
                  aria-describedby={`${idPrefix}-error`}
                  autoComplete="current-password"
                  tabIndex={step === "password" ? 0 : -1}
                  className="h-14 rounded-none bg-transparent px-4 text-lg focus-visible:border-transparent focus-visible:ring-0 aria-invalid:ring-0 aria-invalid:border-transparent aria-invalid:bg-transparent dark:aria-invalid:bg-transparent"
                />
                <SubmitArrow ready={!!password} busy={busy} />
              </div>
            </div>
          </div>
        </div>
        <p
          id={`${idPrefix}-error`}
          aria-live="polite"
          className="mt-2.5 min-h-5.5 text-sm text-destructive"
        >
          {error}
        </p>
        <Label className="mt-1.5 cursor-pointer gap-2 text-sm font-normal">
          <Checkbox defaultChecked />
          Remember me
        </Label>
        <div className="mt-10 flex flex-col items-center gap-2.5 text-sm">
          <a href="#forgot" className="text-primary hover:underline">
            Forgot password? ›
          </a>
          <p className="flex flex-wrap justify-center gap-x-1 text-muted-foreground">
            <span>Don't have a suiss Account?</span>
            <a href="#create" className="text-primary hover:underline">
              Create yours now ›
            </a>
          </p>
        </div>
      </form>
    </section>
  )
}
```

4. Update the import paths to match your project.

## Usage

```tsx
import { LoginAccount } from "@/components/blocks/login-account"
```

```tsx
<LoginAccount />
```

Replace the second step of `submit`, which accepts any password of four or more characters after a delay, with your authentication call, and pass its error message to `fail`. Point the `#forgot` and `#create` links at your routes, and wire the Remember me checkbox, which is uncontrolled. Drop the greeting view if you redirect after login.

## Notes

Inputs use the `username` and `current-password` autocomplete values so password managers work, and errors are tied to the fields and announced.

**React Native, SwiftUI, Jetpack Compose**

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

```tsx title="uniwind/components/blocks/login-account.tsx"
import { cn } from "cn"
import { ArrowRightIcon } from "phosphor-react-native"
import * as React from "react"
import { I18nManager, Pressable, type TextInput, View } from "react-native"
import {
  FadeIn,
  FadeInDown,
  ReduceMotion,
  useAnimatedStyle,
  useSharedValue,
  withSequence,
  withTiming,
} from "react-native-reanimated"
import { AnimatedView } from "@/components/ui/animated-view"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { Icon } from "@/components/ui/icon"
import { Input } from "@/components/ui/input"
import { KeyboardScrollView } from "@/components/ui/keyboard-scroll-view"
import { Label } from "@/components/ui/label"
import { Screen } from "@/components/ui/screen"
import { Spinner } from "@/components/ui/spinner"
import { Text } from "@/components/ui/text"

const EMAIL = /.+@.+\..+/

const SHAKE = [-10, 9, -6, 4, 0]

const STEP = { duration: 70, reduceMotion: ReduceMotion.System }

function useShake() {
  const offset = useSharedValue(0)
  const style = useAnimatedStyle(() => ({
    transform: [{ translateX: offset.value }],
  }))
  const shake = React.useCallback(() => {
    offset.value = withSequence(...SHAKE.map((x) => withTiming(x, STEP)))
  }, [offset])
  return { style, shake }
}

function LinkText({
  onPress,
  className,
  children,
}: {
  onPress?: () => void
  className?: string
  children: React.ReactNode
}) {
  return (
    <Pressable
      role="link"
      hitSlop={8}
      onPress={onPress}
      className="active:opacity-60"
    >
      <Text className={cn("text-primary", className)}>{children}</Text>
    </Pressable>
  )
}

function SubmitArrow({
  ready,
  busy,
  onPress,
}: {
  ready: boolean
  busy: boolean
  onPress: () => void
}) {
  return (
    <Button
      variant="outline"
      size="icon-sm"
      accessibilityLabel="Continue"
      disabled={!ready || busy}
      onPress={onPress}
      className={cn(
        "me-2.5 size-7.5 border-[1.5px] bg-transparent dark:bg-transparent",
        ready && !busy ? "border-muted-foreground" : "border-border"
      )}
    >
      {busy ? (
        <Spinner />
      ) : (
        <Icon
          as={ArrowRightIcon}
          weight="bold"
          className={cn(
            "size-3.5",
            ready ? "text-muted-foreground" : "text-muted-foreground/50",
            I18nManager.isRTL && "rotate-180"
          )}
        />
      )}
    </Button>
  )
}

function LoginAccount({
  onForgotPassword,
  onCreateAccount,
  onSubmit,
}: {
  onForgotPassword?: () => void
  onCreateAccount?: () => void
  onSubmit?: (email: string, password: string) => void
}) {
  const [step, setStep] = React.useState<"email" | "password">("email")
  const [email, setEmail] = React.useState("")
  const [password, setPassword] = React.useState("")
  const [remember, setRemember] = React.useState(true)
  const [error, setError] = React.useState("")
  const [busy, setBusy] = React.useState(false)
  const [signedIn, setSignedIn] = React.useState(false)
  const passwordRef = React.useRef<React.ComponentRef<typeof TextInput>>(null)
  const { style: shakeStyle, shake } = useShake()

  React.useEffect(() => {
    if (!busy) return
    const timer = setTimeout(() => {
      setBusy(false)
      setSignedIn(true)
    }, 1200)
    return () => clearTimeout(timer)
  }, [busy])

  React.useEffect(() => {
    if (step !== "password") return
    const timer = setTimeout(() => passwordRef.current?.focus(), 380)
    return () => clearTimeout(timer)
  }, [step])

  const fail = (message: string) => {
    setError(message)
    shake()
  }

  const submit = () => {
    if (step === "email") {
      if (!EMAIL.test(email)) return fail("Enter a valid suiss Account.")
      setError("")
      setStep("password")
      return
    }
    if (password.length < 4)
      return fail("Your suiss Account or password was incorrect.")
    setError("")
    onSubmit?.(email, password)
    setBusy(true)
  }

  if (signedIn) {
    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"
          >
            <Avatar size="xl" className="size-20">
              <AvatarFallback className="bg-muted dark:bg-muted">
                <Text className="text-3xl text-foreground">{email[0]}</Text>
              </AvatarFallback>
            </Avatar>
            <Text role="heading" className="text-3xl font-semibold">
              Hello.
            </Text>
            <Text className="text-lg text-muted-foreground">{email}</Text>
            <LinkText
              className="text-lg"
              onPress={() => {
                setSignedIn(false)
                setStep("email")
                setPassword("")
              }}
            >
              Sign out ›
            </LinkText>
          </AnimatedView>
        </View>
      </Screen>
    )
  }

  return (
    <Screen>
      <KeyboardScrollView contentContainerClassName="grow justify-center px-6 py-8">
        <View className="w-full items-center">
          <Text
            role="heading"
            className="mb-8 text-center text-3xl font-semibold"
          >
            Sign in with your suiss Account
          </Text>
          <AnimatedView
            style={shakeStyle}
            className={cn(
              "w-full overflow-hidden rounded-xl border bg-background",
              error
                ? "border-destructive bg-destructive/5 dark:bg-destructive/10"
                : "border-input"
            )}
          >
            <View className="flex-row items-center">
              <Input
                value={email}
                onChangeText={(value) => {
                  setEmail(value)
                  setError("")
                  if (step === "password") setStep("email")
                }}
                placeholder="Email or phone number"
                accessibilityLabel="Email or phone number"
                autoComplete="username"
                textContentType="username"
                keyboardType="email-address"
                autoCapitalize="none"
                autoCorrect={false}
                returnKeyType="next"
                submitBehavior="submit"
                onSubmitEditing={submit}
                className="h-14 flex-1 rounded-none border-transparent bg-transparent px-4 text-[18px]"
              />
              {step === "email" ? (
                <SubmitArrow ready={!!email} busy={busy} onPress={submit} />
              ) : null}
            </View>
            {step === "password" ? (
              <AnimatedView
                entering={FadeIn.duration(300).reduceMotion(
                  ReduceMotion.System
                )}
                className="flex-row items-center border-t border-input"
              >
                <Input
                  ref={passwordRef}
                  value={password}
                  onChangeText={(value) => {
                    setPassword(value)
                    setError("")
                  }}
                  placeholder="Password"
                  accessibilityLabel="Password"
                  autoComplete="current-password"
                  textContentType="password"
                  secureTextEntry
                  returnKeyType="go"
                  submitBehavior="submit"
                  onSubmitEditing={submit}
                  className="h-14 flex-1 rounded-none border-transparent bg-transparent px-4 text-[18px]"
                />
                <SubmitArrow ready={!!password} busy={busy} onPress={submit} />
              </AnimatedView>
            ) : null}
          </AnimatedView>
          <Text
            accessibilityLiveRegion="polite"
            className="mt-2.5 min-h-5.5 text-center text-sm text-destructive"
          >
            {error}
          </Text>
          <View className="mt-1.5 flex-row items-center gap-2">
            <Checkbox
              checked={remember}
              onCheckedChange={setRemember}
              accessibilityLabelledBy="login-account-remember"
            />
            <Label
              nativeID="login-account-remember"
              onPress={() => setRemember((current) => !current)}
              className="text-sm font-normal"
            >
              Remember me
            </Label>
          </View>
          <View className="mt-10 items-center gap-2.5">
            <LinkText className="text-sm" onPress={onForgotPassword}>
              Forgot password? ›
            </LinkText>
            <View className="flex-row flex-wrap justify-center gap-x-1">
              <Text className="text-sm text-muted-foreground">
                Don't have a suiss Account?
              </Text>
              <LinkText className="text-sm" onPress={onCreateAccount}>
                Create yours now ›
              </LinkText>
            </View>
          </View>
        </View>
      </KeyboardScrollView>
    </Screen>
  )
}

export { LoginAccount }
```

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

public struct LoginAccount: View {
  enum Step: Sendable {
    case email, password
  }

  enum FocusField: Hashable {
    case email, password
  }

  private var onForgotPassword: (() -> Void)?
  private var onCreateAccount: (() -> Void)?
  private var onSubmit: ((String, String) -> Void)?
  @State private var step: Step
  @State private var email: String
  @State private var password: String
  @State private var error: LocalizedStringKey?
  @State private var busy: Bool
  @State private var signedIn: Bool
  @State private var remember = true
  @State private var shakes = 0
  @FocusState private var focus: FocusField?
  @Environment(\.accessibilityReduceMotion) private var reduceMotion

  public init(
    onSubmit: ((String, String) -> Void)? = nil,
    onForgotPassword: (() -> Void)? = nil,
    onCreateAccount: (() -> Void)? = nil
  ) {
    self.init(step: .email)
    self.onSubmit = onSubmit
    self.onForgotPassword = onForgotPassword
    self.onCreateAccount = onCreateAccount
  }

  init(
    step: Step,
    email: String = "",
    password: String = "",
    error: LocalizedStringKey? = nil,
    busy: Bool = false,
    signedIn: Bool = false
  ) {
    _step = State(initialValue: step)
    _email = State(initialValue: email)
    _password = State(initialValue: password)
    _error = State(initialValue: error)
    _busy = State(initialValue: busy)
    _signedIn = State(initialValue: signedIn)
  }

  public var body: some View {
    Screen {
      if signedIn {
        welcome
          .transition(.opacity.combined(with: .offset(y: 18)))
      } else {
        form
      }
    }
    .animation(reduceMotion ? nil : .timingCurve(0.32, 0.72, 0, 1, duration: 0.45), value: step)
    .animation(reduceMotion ? nil : .timingCurve(0.32, 0.72, 0, 1, duration: 0.6), value: signedIn)
  }

  private var form: some View {
    KeyboardScrollView {
      VStack(spacing: 0) {
        Text("Sign in with your suiss Account")
          .scaledTextStyle(.xl2, weight: .bold)
          .multilineTextAlignment(.center)
          .fixedSize(horizontal: false, vertical: true)
          .padding(.bottom, 32)
        fields
          .shake(trigger: shakes)
        Text(error ?? "")
          .scaledTextStyle(.xs)
          .foregroundStyle(EsuissColor.destructive)
          .frame(minHeight: 22)
          .padding(.top, 10)
          .accessibilityHidden(error == nil)
        Toggle(isOn: $remember) {
          Text("Remember me")
            .scaledFont(.sm)
            .foregroundStyle(EsuissColor.foreground)
        }
        .toggleStyle(.checkbox)
        .padding(.top, 6)
        VStack(spacing: 10) {
          Button("Forgot password? ›") { onForgotPassword?() }
            .buttonStyle(LoginAccountLinkStyle())
          ViewThatFits(in: .horizontal) {
            HStack(spacing: 4) { createAccount }
            VStack(spacing: 4) { createAccount }
          }
        }
        .padding(.top, 40)
      }
      .frame(maxWidth: .infinity)
      .padding(.horizontal, 24)
      .padding(.top, 72)
      .padding(.bottom, 24)
    }
  }

  @ViewBuilder private var createAccount: some View {
    Text("Don't have a suiss Account?")
      .scaledFont(.sm)
      .foregroundStyle(EsuissColor.mutedForeground)
    Button("Create yours now ›") { onCreateAccount?() }
      .buttonStyle(LoginAccountLinkStyle())
  }

  private var fields: some View {
    let shape = RoundedRectangle(cornerRadius: Radius.xl, style: .continuous)
    let invalid = error != nil
    return VStack(spacing: 0) {
      HStack(spacing: 0) {
        TextField("Email or phone number", text: $email)
          .textContentType(.username)
          .keyboardType(.emailAddress)
          .textInputAutocapitalization(.never)
          .autocorrectionDisabled()
          .submitLabel(.continue)
          .focused($focus, equals: .email)
          .onSubmit(submit)
          .onChange(of: email) {
            error = nil
            if step == .password { step = .email }
          }
          .modifier(LoginAccountFieldText())
        if step == .email {
          LoginAccountArrow(ready: !email.isEmpty, busy: busy, action: submit)
        }
      }
      if step == .password {
        EsuissColor.input.frame(height: 1)
        HStack(spacing: 0) {
          SecureField("Password", text: $password)
            .textContentType(.password)
            .submitLabel(.go)
            .focused($focus, equals: .password)
            .onSubmit(submit)
            .onChange(of: password) { error = nil }
            .modifier(LoginAccountFieldText())
          LoginAccountArrow(ready: !password.isEmpty, busy: busy, action: submit)
        }
        .transition(.move(edge: .top).combined(with: .opacity))
      }
    }
    .background(invalid ? Color.adaptive(light: EsuissColor.destructive.opacity(0.1), dark: EsuissColor.destructive.opacity(0.2)) : EsuissColor.background, in: shape)
    .clipShape(shape)
    .overlay {
      shape.strokeBorder(invalid ? EsuissColor.destructive : focus != nil ? EsuissColor.ring : EsuissColor.input, lineWidth: 1)
    }
    .animation(.easeInOut(duration: 0.2), value: invalid)
  }

  private var welcome: some View {
    VStack(spacing: 14) {
      Avatar(size: .xl) {
        Text(verbatim: String(email.prefix(1)).uppercased())
          .scaledFont(.xl2, weight: .semibold)
          .foregroundStyle(EsuissColor.foreground)
          .frame(maxWidth: .infinity, maxHeight: .infinity)
          .background(EsuissColor.muted, in: Circle())
      }
      Text("Hello.")
        .scaledTextStyle(.xl3, weight: .bold)
      Text(verbatim: email)
        .scaledTextStyle(.lg)
        .foregroundStyle(EsuissColor.mutedForeground)
      Button("Sign out ›") {
        signedIn = false
        step = .email
        password = ""
      }
      .buttonStyle(LoginAccountLinkStyle(style: .lg))
    }
    .multilineTextAlignment(.center)
    .padding(24)
    .frame(maxWidth: .infinity, maxHeight: .infinity)
  }

  private func submit() {
    guard !busy else { return }
    if step == .email {
      guard email.wholeMatch(of: /.+@.+\..+/) != nil else {
        return fail("Enter a valid suiss Account.")
      }
      error = nil
      step = .password
      focus = .password
      return
    }
    guard password.count >= 4 else {
      return fail("Your suiss Account or password was incorrect.")
    }
    error = nil
    onSubmit?(email, password)
    busy = true
    focus = nil
    Task {
      try? await Task.sleep(for: .milliseconds(1200))
      busy = false
      signedIn = true
    }
  }

  private func fail(_ message: LocalizedStringKey) {
    error = message
    shakes += 1
  }
}

private struct LoginAccountFieldText: ViewModifier {
  func body(content: Content) -> some View {
    content
      .scaledFont(.lg)
      .foregroundStyle(EsuissColor.foreground)
      .tint(EsuissColor.primary)
      .padding(.horizontal, 16)
      .frame(maxWidth: .infinity, minHeight: 56, alignment: .leading)
  }
}

private struct LoginAccountArrow: View {
  let ready: Bool
  let busy: Bool
  let action: () -> Void

  var body: some View {
    Button(action: action) {
      Group {
        if busy {
          Spinner(.sm)
        } else {
          Ph.arrowRight.bold
            .frame(width: 14, height: 14)
            .flipsForRightToLeftLayoutDirection(true)
        }
      }
      .foregroundStyle(EsuissColor.mutedForeground)
      .frame(width: 30, height: 30)
      .overlay {
        Circle().strokeBorder(ready ? EsuissColor.mutedForeground : EsuissColor.border, lineWidth: 1.5)
      }
      .frame(width: 44, height: 44)
      .contentShape(Rectangle())
    }
    .buttonStyle(.plain)
    .disabled(!ready || busy)
    .opacity(ready || busy ? 1 : 0.6)
    .padding(.trailing, 6)
    .accessibilityLabel(Text("Continue"))
  }
}

private struct LoginAccountLinkStyle: ButtonStyle {
  var style: TextStyle = .sm

  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/LoginAccount.kt"
package com.esuiss.ui.blocks

import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.keyframes
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.BoxWithConstraints
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.layout.widthIn
import androidx.compose.foundation.selection.toggleable
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.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
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.FocusRequester
import androidx.compose.ui.focus.focusRequester
import androidx.compose.ui.graphics.SolidColor
import androidx.compose.ui.graphics.graphicsLayer
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.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.ArrowRight
import com.esuiss.ui.components.Avatar
import com.esuiss.ui.components.AvatarSize
import com.esuiss.ui.components.Checkbox
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.KeyboardScrollView
import com.esuiss.ui.components.Screen
import com.esuiss.ui.components.Spinner
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Radius
import com.esuiss.ui.theme.Typography
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

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

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

private enum class LoginStep { Email, Password }

@Composable
fun LoginAccount(
    modifier: Modifier = Modifier,
    onSubmit: (email: String, password: String) -> Unit = { _, _ -> },
    onForgotPassword: () -> Unit = {},
    onCreateAccount: () -> Unit = {},
) {
    var step by rememberSaveable { mutableStateOf(LoginStep.Email) }
    var email by rememberSaveable { mutableStateOf("") }
    var password by remember { mutableStateOf("") }
    var error by remember { mutableStateOf("") }
    var busy by remember { mutableStateOf(false) }
    var signedIn by rememberSaveable { mutableStateOf(false) }
    var rememberMe by rememberSaveable { mutableStateOf(true) }
    val shake = remember { Animatable(0f) }
    val scope = rememberCoroutineScope()
    val passwordFocus = remember { FocusRequester() }

    fun fail(message: String) {
        error = message
        scope.launch {
            shake.animateTo(
                0f,
                keyframes {
                    durationMillis = 420
                    0f at 0
                    -10f at 84
                    9f at 168
                    -6f at 252
                    4f at 336
                },
            )
        }
    }

    fun submit() {
        if (busy) return
        if (step == LoginStep.Email) {
            if (!EmailPattern.matches(email)) return fail("Enter a valid suiss Account.")
            error = ""
            step = LoginStep.Password
            return
        }
        if (password.length < 4) return fail("Your suiss Account or password was incorrect.")
        error = ""
        onSubmit(email, password)
        busy = true
    }

    LaunchedEffect(busy) {
        if (!busy) return@LaunchedEffect
        delay(1200)
        busy = false
        signedIn = true
    }

    LaunchedEffect(step) {
        if (step != LoginStep.Password) return@LaunchedEffect
        delay(380)
        runCatching { passwordFocus.requestFocus() }
    }

    Screen(modifier) {
        if (signedIn) {
            Greeting(email) {
                signedIn = false
                step = LoginStep.Email
                password = ""
            }
            return@Screen
        }
        CenteredScroll {
            BasicText(
                "Sign in with your suiss Account",
                modifier = Modifier.semantics { heading() },
                style = Typography.xl3.copy(
                    color = EsuissTheme.colors.foreground,
                    fontWeight = FontWeight.SemiBold,
                    textAlign = TextAlign.Center,
                ),
            )
            Spacer(Modifier.height(32.dp))
            FieldStack(
                invalid = error.isNotEmpty(),
                modifier = Modifier.graphicsLayer { translationX = shake.value.dp.toPx() },
            ) {
                Row(verticalAlignment = Alignment.CenterVertically) {
                    StackInput(
                        value = email,
                        onValueChange = {
                            email = it
                            error = ""
                            if (step == LoginStep.Password) step = LoginStep.Email
                        },
                        placeholder = "Email or phone number",
                        invalid = step == LoginStep.Email && error.isNotEmpty(),
                        keyboardType = KeyboardType.Email,
                        imeAction = ImeAction.Next,
                        onAction = ::submit,
                        modifier = Modifier.weight(1f),
                    )
                    if (step == LoginStep.Email) {
                        SubmitArrow(ready = email.isNotEmpty(), busy = busy, onClick = ::submit)
                    }
                }
                AnimatedVisibility(
                    visible = step == LoginStep.Password,
                    enter = expandVertically(tween(450, easing = RevealEasing)),
                    exit = shrinkVertically(tween(450, easing = RevealEasing)),
                ) {
                    Column {
                        Box(
                            Modifier
                                .fillMaxWidth()
                                .height(1.dp)
                                .background(EsuissTheme.colors.input),
                        )
                        Row(verticalAlignment = Alignment.CenterVertically) {
                            StackInput(
                                value = password,
                                onValueChange = {
                                    password = it
                                    error = ""
                                },
                                placeholder = "Password",
                                invalid = step == LoginStep.Password && error.isNotEmpty(),
                                keyboardType = KeyboardType.Password,
                                imeAction = ImeAction.Go,
                                onAction = ::submit,
                                password = true,
                                modifier = Modifier
                                    .weight(1f)
                                    .focusRequester(passwordFocus),
                            )
                            SubmitArrow(ready = password.isNotEmpty(), busy = busy, onClick = ::submit)
                        }
                    }
                }
            }
            BasicText(
                error,
                modifier = Modifier
                    .padding(top = 10.dp)
                    .heightIn(min = 22.dp)
                    .semantics { liveRegion = LiveRegionMode.Polite },
                style = Typography.sm.copy(color = EsuissTheme.colors.destructive, textAlign = TextAlign.Center),
            )
            RememberMe(rememberMe) { rememberMe = it }
            Spacer(Modifier.height(40.dp))
            Column(
                horizontalAlignment = Alignment.CenterHorizontally,
                verticalArrangement = Arrangement.spacedBy(10.dp),
            ) {
                LinkText("Forgot password? ›", onForgotPassword)
                BasicText(
                    "Don't have a suiss Account?",
                    style = Typography.sm.copy(color = EsuissTheme.colors.mutedForeground, textAlign = TextAlign.Center),
                )
                LinkText("Create yours now ›", onCreateAccount)
            }
        }
    }
}

@Composable
private fun CenteredScroll(content: @Composable ColumnScope.() -> Unit) {
    BoxWithConstraints(Modifier.fillMaxSize()) {
        val height = maxHeight
        KeyboardScrollView(modifier = Modifier.fillMaxSize()) {
            Column(
                modifier = Modifier
                    .fillMaxWidth()
                    .heightIn(min = height)
                    .padding(PaddingValues(24.dp)),
                verticalArrangement = Arrangement.Center,
                horizontalAlignment = Alignment.CenterHorizontally,
            ) {
                Column(
                    modifier = Modifier.widthIn(max = 460.dp),
                    horizontalAlignment = Alignment.CenterHorizontally,
                    content = content,
                )
            }
        }
    }
}

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

@Composable
private fun StackInput(
    value: String,
    onValueChange: (String) -> Unit,
    placeholder: String,
    invalid: Boolean,
    keyboardType: KeyboardType,
    imeAction: ImeAction,
    onAction: () -> Unit,
    modifier: Modifier = Modifier,
    password: Boolean = false,
) {
    val colors = EsuissTheme.colors
    val style = Typography.lg.copy(color = colors.foreground)
    BasicTextField(
        value = value,
        onValueChange = onValueChange,
        modifier = modifier
            .height(56.dp)
            .semantics {
                contentDescription = placeholder
                if (invalid) error("Invalid")
            },
        textStyle = style,
        singleLine = true,
        cursorBrush = SolidColor(colors.primary),
        visualTransformation = if (password) PasswordVisualTransformation() else VisualTransformation.None,
        keyboardOptions = KeyboardOptions(keyboardType = keyboardType, imeAction = imeAction),
        keyboardActions = KeyboardActions(onNext = { onAction() }, onGo = { onAction() }),
        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 SubmitArrow(ready: Boolean, busy: Boolean, onClick: () -> Unit) {
    val colors = EsuissTheme.colors
    val enabled = ready && !busy
    val interaction = remember { MutableInteractionSource() }
    val pressed by interaction.collectIsPressedAsState()
    Box(
        modifier = Modifier
            .padding(end = 10.dp)
            .size(30.dp)
            .graphicsLayer {
                alpha = when {
                    !(ready || busy) -> 0.5f
                    pressed -> 0.8f
                    else -> 1f
                }
            }
            .border(1.5.dp, colors.mutedForeground, CircleShape)
            .clickable(
                interactionSource = interaction,
                indication = null,
                enabled = enabled,
                role = Role.Button,
                onClick = onClick,
            )
            .semantics { contentDescription = "Continue" },
        contentAlignment = Alignment.Center,
    ) {
        if (busy) {
            Spinner(size = 14.dp, color = colors.mutedForeground)
        } else {
            Icon(
                PhosphorIcons.Bold.ArrowRight,
                contentDescription = null,
                size = 14.dp,
                tint = colors.mutedForeground,
            )
        }
    }
}

@Composable
private fun RememberMe(checked: Boolean, onCheckedChange: (Boolean) -> Unit) {
    Row(
        modifier = Modifier
            .padding(top = 6.dp)
            .heightIn(min = 44.dp)
            .toggleable(
                value = checked,
                interactionSource = remember { MutableInteractionSource() },
                indication = null,
                role = Role.Checkbox,
                onValueChange = onCheckedChange,
            ),
        horizontalArrangement = Arrangement.spacedBy(8.dp),
        verticalAlignment = Alignment.CenterVertically,
    ) {
        Checkbox(checked = checked, onCheckedChange = null)
        BasicText("Remember me", style = Typography.sm.copy(color = EsuissTheme.colors.foreground))
    }
}

@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 = 4.dp),
        style = Typography.sm.copy(color = EsuissTheme.colors.primary, textAlign = TextAlign.Center),
    )
}

@Composable
private fun Greeting(email: String, onSignOut: () -> 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,
    ) {
        Avatar(
            modifier = Modifier.size(80.dp),
            size = AvatarSize.Xl,
            fallback = {
                Box(
                    Modifier
                        .fillMaxSize()
                        .background(colors.muted, CircleShape),
                    contentAlignment = Alignment.Center,
                ) {
                    BasicText(
                        email.take(1),
                        style = Typography.xl3.copy(color = colors.foreground),
                    )
                }
            },
        )
        BasicText(
            "Hello.",
            modifier = Modifier.semantics { heading() },
            style = Typography.xl3.copy(color = colors.foreground, fontWeight = FontWeight.SemiBold),
        )
        BasicText(email, style = Typography.lg.copy(color = colors.mutedForeground, textAlign = TextAlign.Center))
        LinkText("Sign out ›", onSignOut)
    }
}
```

A sign-in screen inside a `KeyboardScrollView`. The email `Input` comes first; once a valid address is submitted, the password field appears below it and takes focus. Invalid input shakes the form and shows an error. It also has a Remember me `Checkbox` with a `Label`, a Forgot password link, a create account link, and a `Spinner` while signing in, followed by a welcome view with an `Avatar` and a Sign out link.

## Installation

**React Native**

```bash title="Terminal"
npx shadcn@latest add @esuiss-rn/login-account
```

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 login-account
```

**SwiftUI**

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

**Jetpack Compose**

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

The CLI also adds what it builds on: [Avatar](https://ui.suiss.app/docs/components/avatar), [Button](https://ui.suiss.app/docs/components/button), [Checkbox](https://ui.suiss.app/docs/components/checkbox), [Icon](https://ui.suiss.app/docs/components/icon), [Input](https://ui.suiss.app/docs/components/input), [Keyboard Scroll View](https://ui.suiss.app/docs/components/keyboard-scroll-view), [Label](https://ui.suiss.app/docs/components/label), [Screen](https://ui.suiss.app/docs/components/screen), [Spinner](https://ui.suiss.app/docs/components/spinner), [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: [Avatar](https://ui.suiss.app/docs/components/avatar), [Button](https://ui.suiss.app/docs/components/button), [Checkbox](https://ui.suiss.app/docs/components/checkbox), [Icon](https://ui.suiss.app/docs/components/icon), [Input](https://ui.suiss.app/docs/components/input), [Keyboard Scroll View](https://ui.suiss.app/docs/components/keyboard-scroll-view), [Label](https://ui.suiss.app/docs/components/label), [Screen](https://ui.suiss.app/docs/components/screen), [Spinner](https://ui.suiss.app/docs/components/spinner), [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/login-account.tsx`:

- `uniwind/components/blocks/login-account.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: [Avatar](https://ui.suiss.app/docs/components/avatar), [Button](https://ui.suiss.app/docs/components/button), [Checkbox](https://ui.suiss.app/docs/components/checkbox), [Icon](https://ui.suiss.app/docs/components/icon), [Input](https://ui.suiss.app/docs/components/input), [Keyboard Scroll View](https://ui.suiss.app/docs/components/keyboard-scroll-view), [Label](https://ui.suiss.app/docs/components/label), [Screen](https://ui.suiss.app/docs/components/screen), [Spinner](https://ui.suiss.app/docs/components/spinner), [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/login-account.tsx`:

```tsx title="nativewind/components/blocks/login-account.tsx"
import { cn } from "cn"
import { ArrowRightIcon } from "phosphor-react-native"
import * as React from "react"
import { I18nManager, Pressable, type TextInput, View } from "react-native"
import {
  FadeIn,
  FadeInDown,
  ReduceMotion,
  useAnimatedStyle,
  useSharedValue,
  withSequence,
  withTiming,
} from "react-native-reanimated"
import { AnimatedView } from "@/components/ui/animated-view"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { Icon } from "@/components/ui/icon"
import { Input } from "@/components/ui/input"
import { KeyboardScrollView } from "@/components/ui/keyboard-scroll-view"
import { Label } from "@/components/ui/label"
import { Screen } from "@/components/ui/screen"
import { Spinner } from "@/components/ui/spinner"
import { Text } from "@/components/ui/text"

const EMAIL = /.+@.+\..+/

const SHAKE = [-10, 9, -6, 4, 0]

const STEP = { duration: 70, reduceMotion: ReduceMotion.System }

function useShake() {
  const offset = useSharedValue(0)
  const style = useAnimatedStyle(() => ({
    transform: [{ translateX: offset.value }],
  }))
  const shake = React.useCallback(() => {
    offset.value = withSequence(...SHAKE.map((x) => withTiming(x, STEP)))
  }, [offset])
  return { style, shake }
}

function LinkText({
  onPress,
  className,
  children,
}: {
  onPress?: () => void
  className?: string
  children: React.ReactNode
}) {
  return (
    <Pressable
      role="link"
      hitSlop={8}
      onPress={onPress}
      className="active:opacity-60"
    >
      <Text className={cn("text-primary", className)}>{children}</Text>
    </Pressable>
  )
}

function SubmitArrow({
  ready,
  busy,
  onPress,
}: {
  ready: boolean
  busy: boolean
  onPress: () => void
}) {
  return (
    <Button
      variant="outline"
      size="icon-sm"
      accessibilityLabel="Continue"
      disabled={!ready || busy}
      onPress={onPress}
      className={cn(
        "me-2.5 size-[1.875rem] border-[1.5px] bg-transparent dark:bg-transparent",
        ready && !busy ? "border-muted-foreground" : "border-border"
      )}
    >
      {busy ? (
        <Spinner />
      ) : (
        <Icon
          as={ArrowRightIcon}
          weight="bold"
          className={cn(
            "size-3.5",
            ready ? "text-muted-foreground" : "text-muted-foreground-50",
            I18nManager.isRTL && "rotate-180"
          )}
        />
      )}
    </Button>
  )
}

function LoginAccount({
  onForgotPassword,
  onCreateAccount,
  onSubmit,
}: {
  onForgotPassword?: () => void
  onCreateAccount?: () => void
  onSubmit?: (email: string, password: string) => void
}) {
  const [step, setStep] = React.useState<"email" | "password">("email")
  const [email, setEmail] = React.useState("")
  const [password, setPassword] = React.useState("")
  const [remember, setRemember] = React.useState(true)
  const [error, setError] = React.useState("")
  const [busy, setBusy] = React.useState(false)
  const [signedIn, setSignedIn] = React.useState(false)
  const passwordRef = React.useRef<React.ComponentRef<typeof TextInput>>(null)
  const { style: shakeStyle, shake } = useShake()

  React.useEffect(() => {
    if (!busy) return
    const timer = setTimeout(() => {
      setBusy(false)
      setSignedIn(true)
    }, 1200)
    return () => clearTimeout(timer)
  }, [busy])

  React.useEffect(() => {
    if (step !== "password") return
    const timer = setTimeout(() => passwordRef.current?.focus(), 380)
    return () => clearTimeout(timer)
  }, [step])

  const fail = (message: string) => {
    setError(message)
    shake()
  }

  const submit = () => {
    if (step === "email") {
      if (!EMAIL.test(email)) return fail("Enter a valid suiss Account.")
      setError("")
      setStep("password")
      return
    }
    if (password.length < 4)
      return fail("Your suiss Account or password was incorrect.")
    setError("")
    onSubmit?.(email, password)
    setBusy(true)
  }

  if (signedIn) {
    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"
          >
            <Avatar size="xl" className="size-20">
              <AvatarFallback className="bg-muted dark:bg-muted">
                <Text className="text-3xl text-foreground">{email[0]}</Text>
              </AvatarFallback>
            </Avatar>
            <Text role="heading" className="text-3xl font-semibold">
              Hello.
            </Text>
            <Text className="text-lg text-muted-foreground">{email}</Text>
            <LinkText
              className="text-lg"
              onPress={() => {
                setSignedIn(false)
                setStep("email")
                setPassword("")
              }}
            >
              Sign out ›
            </LinkText>
          </AnimatedView>
        </View>
      </Screen>
    )
  }

  return (
    <Screen>
      <KeyboardScrollView contentContainerClassName="grow justify-center px-6 py-8">
        <View className="w-full items-center">
          <Text
            role="heading"
            className="mb-8 text-center text-3xl font-semibold"
          >
            Sign in with your suiss Account
          </Text>
          <AnimatedView
            style={shakeStyle}
            className={cn(
              "w-full overflow-hidden rounded-xl border bg-background",
              error
                ? "border-destructive bg-destructive-5 dark:bg-destructive-10"
                : "border-input"
            )}
          >
            <View className="flex-row items-center">
              <Input
                value={email}
                onChangeText={(value) => {
                  setEmail(value)
                  setError("")
                  if (step === "password") setStep("email")
                }}
                placeholder="Email or phone number"
                accessibilityLabel="Email or phone number"
                autoComplete="username"
                textContentType="username"
                keyboardType="email-address"
                autoCapitalize="none"
                autoCorrect={false}
                returnKeyType="next"
                submitBehavior="submit"
                onSubmitEditing={submit}
                className="h-14 flex-1 rounded-none border-transparent bg-transparent px-4 text-[18px]"
              />
              {step === "email" ? (
                <SubmitArrow ready={!!email} busy={busy} onPress={submit} />
              ) : null}
            </View>
            {step === "password" ? (
              <AnimatedView
                entering={FadeIn.duration(300).reduceMotion(
                  ReduceMotion.System
                )}
                className="flex-row items-center border-t border-input"
              >
                <Input
                  ref={passwordRef}
                  value={password}
                  onChangeText={(value) => {
                    setPassword(value)
                    setError("")
                  }}
                  placeholder="Password"
                  accessibilityLabel="Password"
                  autoComplete="current-password"
                  textContentType="password"
                  secureTextEntry
                  returnKeyType="go"
                  submitBehavior="submit"
                  onSubmitEditing={submit}
                  className="h-14 flex-1 rounded-none border-transparent bg-transparent px-4 text-[18px]"
                />
                <SubmitArrow ready={!!password} busy={busy} onPress={submit} />
              </AnimatedView>
            ) : null}
          </AnimatedView>
          <Text
            accessibilityLiveRegion="polite"
            className="mt-2.5 min-h-[1.375rem] text-center text-sm text-destructive"
          >
            {error}
          </Text>
          <View className="mt-1.5 flex-row items-center gap-2">
            <Checkbox
              checked={remember}
              onCheckedChange={setRemember}
              accessibilityLabelledBy="login-account-remember"
            />
            <Label
              nativeID="login-account-remember"
              onPress={() => setRemember((current) => !current)}
              className="text-sm font-normal"
            >
              Remember me
            </Label>
          </View>
          <View className="mt-10 items-center gap-2.5">
            <LinkText className="text-sm" onPress={onForgotPassword}>
              Forgot password? ›
            </LinkText>
            <View className="flex-row flex-wrap justify-center gap-x-1">
              <Text className="text-sm text-muted-foreground">
                Don't have a suiss Account?
              </Text>
              <LinkText className="text-sm" onPress={onCreateAccount}>
                Create yours now ›
              </LinkText>
            </View>
          </View>
        </View>
      </KeyboardScrollView>
    </Screen>
  )
}

export { LoginAccount }
```

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: [Avatar](https://ui.suiss.app/docs/components/avatar), [Button](https://ui.suiss.app/docs/components/button), [Checkbox](https://ui.suiss.app/docs/components/checkbox), [Icon](https://ui.suiss.app/docs/components/icon), [Input](https://ui.suiss.app/docs/components/input), [Keyboard Scroll View](https://ui.suiss.app/docs/components/keyboard-scroll-view), [Label](https://ui.suiss.app/docs/components/label), [Screen](https://ui.suiss.app/docs/components/screen), [Spinner](https://ui.suiss.app/docs/components/spinner), [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/login-account.tsx`:

```tsx title="stylesheet/components/blocks/login-account.tsx"
import { ArrowRightIcon } from "phosphor-react-native"
import * as React from "react"
import {
  I18nManager,
  Pressable,
  StyleSheet,
  type TextInput,
  type TextStyle,
  View,
} from "react-native"
import {
  FadeIn,
  FadeInDown,
  ReduceMotion,
  useAnimatedStyle,
  useSharedValue,
  withSequence,
  withTiming,
} from "react-native-reanimated"
import { AnimatedView } from "@/components/ui/animated-view"
import { Avatar, AvatarFallback } from "@/components/ui/avatar"
import { Button } from "@/components/ui/button"
import { Checkbox } from "@/components/ui/checkbox"
import { Icon } from "@/components/ui/icon"
import { Input } from "@/components/ui/input"
import { KeyboardScrollView } from "@/components/ui/keyboard-scroll-view"
import { Label } from "@/components/ui/label"
import { Screen } from "@/components/ui/screen"
import { Spinner } from "@/components/ui/spinner"
import { Text } from "@/components/ui/text"
import { fontSize, radius, useTheme } from "@/components/ui/theme"

const EMAIL = /.+@.+\..+/

const SHAKE = [-10, 9, -6, 4, 0]

const STEP = { duration: 70, reduceMotion: ReduceMotion.System }

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({
  center: {
    flex: 1,
    alignItems: "center",
    justifyContent: "center",
    padding: 24,
  },
  done: { alignItems: "center", gap: 14 },
  scroll: {
    flexGrow: 1,
    justifyContent: "center",
    paddingHorizontal: 24,
    paddingVertical: 32,
  },
  form: { width: "100%", alignItems: "center" },
  title: {
    ...fontSize["3xl"],
    marginBottom: 32,
    textAlign: "center",
    fontWeight: "600",
  },
  fields: {
    width: "100%",
    overflow: "hidden",
    borderRadius: radius.xl,
    borderWidth: 1,
  },
  row: { flexDirection: "row", alignItems: "center" },
  input: {
    fontSize: fontSize.lg.fontSize,
    height: 56,
    flex: 1,
    borderRadius: 0,
    borderColor: "transparent",
    backgroundColor: "transparent",
    paddingHorizontal: 16,
  },
  arrow: {
    marginEnd: 10,
    width: 30,
    height: 30,
    borderWidth: 1.5,
    backgroundColor: "transparent",
  },
  error: {
    ...fontSize.sm,
    marginTop: 10,
    minHeight: 22,
    textAlign: "center",
  },
  remember: {
    marginTop: 6,
    flexDirection: "row",
    alignItems: "center",
    gap: 8,
  },
  links: { marginTop: 40, alignItems: "center", gap: 10 },
  wrap: {
    flexDirection: "row",
    flexWrap: "wrap",
    justifyContent: "center",
    columnGap: 4,
  },
})

function useShake() {
  const offset = useSharedValue(0)
  const style = useAnimatedStyle(() => ({
    transform: [{ translateX: offset.value }],
  }))
  const shake = React.useCallback(() => {
    offset.value = withSequence(...SHAKE.map((x) => withTiming(x, STEP)))
  }, [offset])
  return { style, shake }
}

function LinkText({
  onPress,
  style,
  children,
}: {
  onPress?: () => void
  style?: TextStyle
  children: React.ReactNode
}) {
  const { colors } = useTheme()
  return (
    <Pressable
      role="link"
      hitSlop={8}
      onPress={onPress}
      style={({ pressed }) => pressed && { opacity: 0.6 }}
    >
      <Text style={[{ color: colors.primary }, style]}>{children}</Text>
    </Pressable>
  )
}

function SubmitArrow({
  ready,
  busy,
  onPress,
}: {
  ready: boolean
  busy: boolean
  onPress: () => void
}) {
  const { colors } = useTheme()
  return (
    <Button
      variant="outline"
      size="icon-sm"
      accessibilityLabel="Continue"
      disabled={!ready || busy}
      onPress={onPress}
      style={[
        styles.arrow,
        {
          borderColor: colors.mutedForeground,
        },
      ]}
    >
      {busy ? (
        <Spinner />
      ) : (
        <Icon
          as={ArrowRightIcon}
          weight="bold"
          size={14}
          color={colors.mutedForeground}
          style={
            I18nManager.isRTL ? { transform: [{ rotate: "180deg" }] } : null
          }
        />
      )}
    </Button>
  )
}

function LoginAccount({
  onForgotPassword,
  onCreateAccount,
  onSubmit,
}: {
  onForgotPassword?: () => void
  onCreateAccount?: () => void
  onSubmit?: (email: string, password: string) => void
}) {
  const { colors, isDark } = useTheme()
  const [step, setStep] = React.useState<"email" | "password">("email")
  const [email, setEmail] = React.useState("")
  const [password, setPassword] = React.useState("")
  const [remember, setRemember] = React.useState(true)
  const [error, setError] = React.useState("")
  const [busy, setBusy] = React.useState(false)
  const [signedIn, setSignedIn] = React.useState(false)
  const passwordRef = React.useRef<React.ComponentRef<typeof TextInput>>(null)
  const { style: shakeStyle, shake } = useShake()

  React.useEffect(() => {
    if (!busy) return
    const timer = setTimeout(() => {
      setBusy(false)
      setSignedIn(true)
    }, 1200)
    return () => clearTimeout(timer)
  }, [busy])

  React.useEffect(() => {
    if (step !== "password") return
    const timer = setTimeout(() => passwordRef.current?.focus(), 380)
    return () => clearTimeout(timer)
  }, [step])

  const fail = (message: string) => {
    setError(message)
    shake()
  }

  const submit = () => {
    if (step === "email") {
      if (!EMAIL.test(email)) return fail("Enter a valid suiss Account.")
      setError("")
      setStep("password")
      return
    }
    if (password.length < 4)
      return fail("Your suiss Account or password was incorrect.")
    setError("")
    onSubmit?.(email, password)
    setBusy(true)
  }

  if (signedIn) {
    return (
      <Screen>
        <View style={styles.center}>
          <AnimatedView
            entering={FadeInDown.duration(600).reduceMotion(
              ReduceMotion.System
            )}
            style={styles.done}
          >
            <Avatar size="xl" style={{ width: 80, height: 80 }}>
              <AvatarFallback style={{ backgroundColor: colors.muted }}>
                <Text style={{ ...fontSize["3xl"], color: colors.foreground }}>
                  {email[0]}
                </Text>
              </AvatarFallback>
            </Avatar>
            <Text
              role="heading"
              style={{ ...fontSize["3xl"], fontWeight: "600" }}
            >
              Hello.
            </Text>
            <Text style={{ ...fontSize.lg, color: colors.mutedForeground }}>
              {email}
            </Text>
            <LinkText
              style={fontSize.lg}
              onPress={() => {
                setSignedIn(false)
                setStep("email")
                setPassword("")
              }}
            >
              Sign out ›
            </LinkText>
          </AnimatedView>
        </View>
      </Screen>
    )
  }

  return (
    <Screen>
      <KeyboardScrollView contentContainerStyle={styles.scroll}>
        <View style={styles.form}>
          <Text role="heading" style={styles.title}>
            Sign in with your suiss Account
          </Text>
          <AnimatedView
            style={[
              styles.fields,
              error
                ? {
                    borderColor: colors.destructive,
                    backgroundColor: withAlpha(
                      colors.destructive,
                      isDark ? 0.1 : 0.05
                    ),
                  }
                : {
                    borderColor: colors.input,
                    backgroundColor: colors.background,
                  },
              shakeStyle,
            ]}
          >
            <View style={styles.row}>
              <Input
                value={email}
                onChangeText={(value) => {
                  setEmail(value)
                  setError("")
                  if (step === "password") setStep("email")
                }}
                placeholder="Email or phone number"
                accessibilityLabel="Email or phone number"
                autoComplete="username"
                textContentType="username"
                keyboardType="email-address"
                autoCapitalize="none"
                autoCorrect={false}
                returnKeyType="next"
                submitBehavior="submit"
                onSubmitEditing={submit}
                style={styles.input}
              />
              {step === "email" ? (
                <SubmitArrow ready={!!email} busy={busy} onPress={submit} />
              ) : null}
            </View>
            {step === "password" ? (
              <AnimatedView
                entering={FadeIn.duration(300).reduceMotion(
                  ReduceMotion.System
                )}
                style={[
                  styles.row,
                  {
                    borderTopWidth: 1,
                    borderTopColor: colors.input,
                  },
                ]}
              >
                <Input
                  ref={passwordRef}
                  value={password}
                  onChangeText={(value) => {
                    setPassword(value)
                    setError("")
                  }}
                  placeholder="Password"
                  accessibilityLabel="Password"
                  autoComplete="current-password"
                  textContentType="password"
                  secureTextEntry
                  returnKeyType="go"
                  submitBehavior="submit"
                  onSubmitEditing={submit}
                  style={styles.input}
                />
                <SubmitArrow ready={!!password} busy={busy} onPress={submit} />
              </AnimatedView>
            ) : null}
          </AnimatedView>
          <Text
            accessibilityLiveRegion="polite"
            style={[styles.error, { color: colors.destructive }]}
          >
            {error}
          </Text>
          <View style={styles.remember}>
            <Checkbox
              checked={remember}
              onCheckedChange={setRemember}
              accessibilityLabelledBy="login-account-remember"
            />
            <Label
              nativeID="login-account-remember"
              onPress={() => setRemember((current) => !current)}
              style={{ ...fontSize.sm, fontWeight: "400" }}
            >
              Remember me
            </Label>
          </View>
          <View style={styles.links}>
            <LinkText style={fontSize.sm} onPress={onForgotPassword}>
              Forgot password? ›
            </LinkText>
            <View style={styles.wrap}>
              <Text style={{ ...fontSize.sm, color: colors.mutedForeground }}>
                Don't have a suiss Account?
              </Text>
              <LinkText style={fontSize.sm} onPress={onCreateAccount}>
                Create yours now ›
              </LinkText>
            </View>
          </View>
        </View>
      </KeyboardScrollView>
    </Screen>
  )
}

export { LoginAccount }
```

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/LoginAccount.swift`: shown above

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

- `swift/Components/Checkbox.swift`: see [Checkbox](https://ui.suiss.app/docs/components/checkbox.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/KeyboardScrollView.swift`: see [Keyboard Scroll View](https://ui.suiss.app/docs/components/keyboard-scroll-view.md)

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

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

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

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

- `swift/Components/TextVariant.swift`: [source](https://github.com/e-suiss/ui/blob/main/packages/mobile/swiftui/Sources/EsuissUI/Components/TextVariant.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/LoginAccount.kt`: shown above

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

- `kotlin/components/Checkbox.kt`: see [Checkbox](https://ui.suiss.app/docs/components/checkbox.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/Haptics.kt`: see [Haptics](https://ui.suiss.app/docs/components/haptics.md)

- `kotlin/components/Icon.kt`: see [Icon](https://ui.suiss.app/docs/components/icon.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/Screen.kt`: see [Screen](https://ui.suiss.app/docs/components/screen.md)

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

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

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

## Usage

```tsx
import { LoginAccount } from "@/components/blocks/login-account"
```

```tsx
<LoginAccount />
```

Wire `onSubmit` (receives the email and password) to your auth service, and `onForgotPassword` and `onCreateAccount` to your navigation. The sign-in delay and the success view are simulated in the component.

