v0.1 public beta early access on GitHub
Home / Screens / auth-login
Screen template

Login Screen

Email/password sign-in with validation, remember me, social buttons, and forgot-password link

pro Expo Go ✓ iosandroid gestures: tap haptics: light · medium · selection · error
$ npx lumioui add auth-login
import { LoginScreen } from "@/screens/auth/login";
Native packages installed6
@lumioui/ui@latest@lumioui/core@latestreact-native-safe-area-context@*react-native-reanimated@*react-native-svg@*nativewind@*
auth/login.tsxsource · yours to edit
import * as React from "react";
import { View, Pressable, ScrollView, TextInput } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useRef, useState } from "react";
import { Button, Checkbox, Divider, Glass, Icon, Input, KeyboardAvoiding, Text } from "@lumioui/ui";
import { useHaptic, useTheme } from "@lumioui/core";
import { AppleLogo, BLOCK_STAGGER, GoogleLogo, HeroGradient, Mount, World } from "../shared";

export interface LoginScreenProps {
  onLogin?: (email: string, password: string, remember: boolean) => void;
  onSignUp?: () => void;
  onForgotPassword?: () => void;
  onSocialLogin?: (provider: "apple" | "google") => void;
  logo?: React.ReactNode;
  /** Prefilled field values for demos/previews — production hosts omit
   *  these so the form starts empty. */
  defaultEmail?: string;
  defaultPassword?: string;
  defaultRemember?: boolean;
}

interface FieldErrors {
  email?: string;
  password?: string;
}

/** Login screen — icon-leading email/password fields, social providers,
 *  remember me, and a staggered mount entrance. */
export function LoginScreen({ onLogin, onSignUp, onForgotPassword, onSocialLogin, logo, defaultEmail, defaultPassword, defaultRemember }: LoginScreenProps) {
  const haptics = useHaptic();
  const insets = useSafeAreaInsets();
  const { colors } = useTheme();
  const passwordRef = useRef<TextInput>(null);
  const [email, setEmail] = useState(defaultEmail ?? "");
  const [password, setPassword] = useState(defaultPassword ?? "");
  const [showPassword, setShowPassword] = useState(false);
  const [remember, setRemember] = useState(defaultRemember ?? false);
  const [loading, setLoading] = useState(false);
  const [errors, setErrors] = useState<FieldErrors>({});

  const canSubmit = email.trim().length > 0 && password.length > 0;

  const submit = () => {
    const e: FieldErrors = {};
    if (!email.includes("@")) e.email = "Enter a valid email address";
    if (password.length < 6) e.password = "Password must be at least 6 characters";
    setErrors(e);
    if (Object.keys(e).length) {
      haptics.error();
      return;
    }
    setLoading(true);
    onLogin?.(email, password, remember);
    setTimeout(() => setLoading(false), 800);
  };

  return (
    <World primary="#65A30D">
    <View className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]">
      {/* Ambient color field behind the form — glass surfaces need something
          to refract, so the brand hue bleeds down past the hero. */}
      <KeyboardAvoiding>
        <ScrollView
          showsVerticalScrollIndicator={false}
          keyboardShouldPersistTaps="handled"
          keyboardDismissMode="interactive"
          contentContainerClassName="flex-grow px-6"
          contentContainerStyle={{ paddingBottom: insets.bottom + 24 }}
        >
          {/* Brand hero — deep indigo wash bleeding to the top edge (the
              -mx-6 cancels the scroll padding). Signature moment: raw hex
              is art direction here, like StoryChrome. */}
          <Mount delay={0} className="-mx-6">
            <View className="overflow-hidden bg-[#1B1B26] px-6 pb-7" style={{ paddingTop: insets.top + 28 }}>
              <HeroGradient from="#23232E" to="#14141A" />
              <View className="gap-4">
                {logo ?? (
                  <Glass interactive effect="clear" className="h-12 w-12 items-center justify-center rounded-2xl">
                    <Icon name="zap" size="lg" color="#FFFFFF" />
                  </Glass>
                )}
                <View className="gap-2">
                  <Text className="text-[34px] font-bold leading-[38px] tracking-tight text-white">
                    Design systems{"\n"}for touch.
                  </Text>
                  <Text className="max-w-[240px] text-[15px] leading-relaxed text-white/70">
                    Springs, haptics, and choreography — shipped standard,
                    not bolted on.
                  </Text>
                </View>
                <View className="flex-row gap-2 pt-1">
                  {["Springs", "Haptics", "Motion"].map((chip) => (
                    <Glass interactive key={chip} effect="clear" sheen={0.6} className="rounded-full px-3 py-1.5">
                      <Text className="text-xs font-medium text-white">{chip}</Text>
                    </Glass>
                  ))}
                </View>
              </View>
            </View>
          </Mount>

          {/* Form — fields stagger in under the hero */}
          <View className="mt-8 gap-4">
            <Mount delay={0}>
              <Text variant="h2" className="tracking-tight">Welcome <Text className="italic text-[#65A30D]">back</Text></Text>
            </Mount>
            <Mount delay={BLOCK_STAGGER}>
              <Input
                glass
                label="Email"
                placeholder="you@example.com"
                leftIcon={<Icon name="mail" size="sm" color="mutedForeground" />}
                value={email}
                onChangeText={(v) => {
                  setEmail(v);
                  if (errors.email) setErrors((e) => ({ ...e, email: undefined }));
                }}
                keyboardType="email-address"
                autoCapitalize="none"
                autoComplete="email"
                textContentType="emailAddress"
                returnKeyType="next"
                onSubmitEditing={() => passwordRef.current?.focus()}
                error={errors.email}
              />
            </Mount>
            <Mount delay={BLOCK_STAGGER * 2}>
              <View>
                <Input
                  glass
                  ref={passwordRef}
                  label="Password"
                  placeholder="Your password"
                  leftIcon={<Icon name="lock" size="sm" color="mutedForeground" />}
                  value={password}
                  onChangeText={(v) => {
                    setPassword(v);
                    if (errors.password) setErrors((e) => ({ ...e, password: undefined }));
                  }}
                  secureTextEntry={!showPassword}
                  autoComplete="password"
                  textContentType="password"
                  returnKeyType="done"
                  onSubmitEditing={submit}
                  error={errors.password}
                  className="pr-11"
                />
                {/* Visibility toggle — overlays the field's right edge; pr-11 above reserves the space. */}
                <Pressable
                  onPress={() => {
                    haptics.selection();
                    setShowPassword((v) => !v);
                  }}
                  accessibilityRole="button"
                  accessibilityLabel={showPassword ? "Hide password" : "Show password"}
                  accessibilityState={{ selected: showPassword }}
                  hitSlop={4}
                  className="absolute right-1 top-7 h-11 w-11 items-center justify-center rounded-full"
                >
                  <Icon name={showPassword ? "eye-off" : "eye"} size="sm" color="mutedForeground" />
                </Pressable>
              </View>
            </Mount>

            <Mount delay={BLOCK_STAGGER * 3}>
              <View className="flex-row items-center justify-between">
                {/* Whole row toggles — the checkbox inside is visual-only for a11y,
                    so there's a single 44pt "Remember me" checkbox stop. */}
                <Pressable
                  onPress={() => {
                    haptics.selection();
                    setRemember((v) => !v);
                  }}
                  accessibilityRole="checkbox"
                  accessibilityState={{ checked: remember }}
                  accessibilityLabel="Remember me"
                  hitSlop={8}
                  className="min-h-11 flex-row items-center gap-2"
                >
                  <Checkbox
                    checked={remember}
                    onCheckedChange={setRemember}
                    accessible={false}
                    accessibilityElementsHidden
                    importantForAccessibility="no"
                  />
                  <Text variant="small" className="text-muted-foreground">
                    Remember me
                  </Text>
                </Pressable>
                <Pressable
                  onPress={() => {
                    haptics.light();
                    onForgotPassword?.();
                  }}
                  accessibilityRole="link"
                  accessibilityLabel="Forgot password"
                  hitSlop={8}
                  className="min-h-11 justify-center"
                >
                  <Text variant="small" className="text-[#65A30D]">
                    Forgot password?
                  </Text>
                </Pressable>
              </View>
            </Mount>

            <Mount delay={BLOCK_STAGGER * 4}>
              <Button
                size="lg"
                loading={loading}
                disabled={!canSubmit}
                onPress={submit}
                haptic="medium"
                className="bg-[#65A30D] w-full"
              >
                Sign in
              </Button>
              {/* Returning-user fast path — biometric sign-in rides under the
                  primary button on every banking app. */}
              <Pressable
                onPress={() => {
                  haptics.medium();
                  onLogin?.("", "", true);
                }}
                accessibilityRole="button"
                accessibilityLabel="Sign in with Face ID"
                hitSlop={8}
                className="mt-1 min-h-11 flex-row items-center justify-center gap-2 self-center overflow-hidden rounded-full px-5"
              >
                <Glass interactive effect="clear" sheen={0.5} className="absolute inset-0 rounded-full" pointerEvents="none" />
                <Icon name="scan-face" size="sm" color="#65A30D" />
                <Text variant="small" className="font-medium text-[#65A30D]">
                  Sign in with Face ID
                </Text>
              </Pressable>
            </Mount>
          </View>

          {/* Divider — hairline with quiet caps label */}
          <Mount delay={BLOCK_STAGGER * 5} className="mt-9">
            <Divider
              label="or continue with"
              labelClassName="text-xs uppercase tracking-widest"
            />
          </Mount>

          {/* Social providers — liquid-glass pills with the real brand marks. */}
          <Mount delay={BLOCK_STAGGER * 6}>
            <View className="mt-7 flex-row gap-3">
              <Button
                variant="glass"
                className="flex-1"
                icon={<GoogleLogo />}
                onPress={() => onSocialLogin?.("google")}
                accessibilityLabel="Continue with Google"
              >
                Google
              </Button>
              <Button
                variant="glass"
                className="flex-1"
                icon={<AppleLogo color={colors.foreground} />}
                onPress={() => onSocialLogin?.("apple")}
                accessibilityLabel="Continue with Apple"
              >
                Apple
              </Button>
            </View>
          </Mount>

          {/* Footer — pinned to the bottom of the viewport */}
          <Mount delay={BLOCK_STAGGER * 7} className="mt-auto">
            <View className="items-center pt-8">
              <Pressable
                onPress={() => {
                  haptics.light();
                  onSignUp?.();
                }}
                className="min-h-11 items-center justify-center px-2"
                accessibilityRole="link"
                accessibilityLabel="Sign up for a new account"
                hitSlop={8}
              >
                <Text variant="muted">
                  Don&apos;t have an account?{" "}
                  <Text className="text-[#65A30D] font-semibold">Sign up</Text>
                </Text>
              </Pressable>
            </View>
          </Mount>
        </ScrollView>
      </KeyboardAvoiding>
    </View>
    </World>
  );
}
agent prompt — paste into Claude Code, Cursor, Devin…
Recreate the "Login Screen" screen in React Native using LumioUI.

Goal: Email/password sign-in with validation, remember me, social buttons, and forgot-password link Match the reference implementation's layout, behavior, and motion exactly — this is a production screen, not a mockup.

## Setup

Fastest path — install the screen plus its component deps:

```bash
npx lumioui add auth-login
```

Or pull just the components: `npx lumioui add button checkbox divider glass icon input keyboard-avoiding text`

Native packages:
- @lumioui/ui@latest
- @lumioui/core@latest
- react-native-safe-area-context@*
- react-native-reanimated@*
- react-native-svg@*
- nativewind@*

Stack assumptions: Expo SDK 54 / RN 0.81, Reanimated 4, NativeWind v4, TypeScript strict, Expo Go compatible.

## Components — how, where, when

### `button` — Tactile pill button with press spring, ink wash and async loading.
- `<Button>` ×3

### `checkbox` — Animated check box with haptic tick and error state.
- `<Checkbox>` ×1

### `divider` — Hairline rule with optional centered label, horizontal or vertical.
- `<Divider>` ×1

### `glass` — iOS 26 liquid-glass surface — real glass via expo-glass-effect (Expo Go) or @callstack/liquid-glass (dev builds), SVG refraction + specular rim on web, frosted fallback elsewhere; GlassContainer melts adjacent panes into one blob
- `<Glass>` ×3

### `icon` — Lucide icon set mapped to theme colors and sizes.
- `<Icon>` ×5

### `input` — Labeled field with focus ring, error shake and success check.
- `<Input>` ×2

### `keyboard-avoiding` — View that lifts above the keyboard on both platforms.
- `<KeyboardAvoiding>` ×1 — Ambient color field behind the form — glass surfaces need something to refract, so the brand hue bleeds down past the hero.

### `text` — Themed type scale from caption to display.
- `<Text>` ×9

## Conventions (non-negotiable)

- Styling via NativeWind `className` with semantic tokens only: `bg-background`, `text-foreground`, `bg-card`, `text-card-foreground`, `bg-secondary`, `bg-muted`, `text-muted-foreground`, `bg-accent`, `bg-destructive`, `border-border`. Hard-coded colors only inside media overlays (`text-white`, `bg-black/N`).
- Motion via the token presets from `@lumioui/core` (`spring.press`, `spring.surface`, `spring.glide`, `timing.fast`, `timing.slow`, …) — no bespoke spring configs or durations.
- Gate transform motion behind `useReducedMotion()`; keep opacity/color feedback, drop travel and overshoot.
- Haptics via `useHaptic()`: `light` on press, `selection` on toggle/select, `warning` on invalid input. Never import expo-haptics directly.
- Press feedback via `usePressAnimation()` (~0.97 scale), not `activeOpacity` tricks.
- a11y: `accessibilityRole` + `accessibilityLabel` on every control, touch targets ≥44pt.
- Entrance choreography: staggered `Mount` wrappers (delay 0/60/120…) so regions land in sequence instead of popping in at once.
- Wrap the screen in `SafeAreaView`; keep dark mode working via the semantic tokens.

## Deliverable

One file: `screens/auth/login.tsx` exporting LoginScreen. Preserve the props interface — the host app wires the callbacks (navigation, submit handlers) through them.

## Reference implementation

The LumioUI source for this screen — reproduce it faithfully:

```tsx
import * as React from "react";
import { View, Pressable, ScrollView, TextInput } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useRef, useState } from "react";
import { Button, Checkbox, Divider, Glass, Icon, Input, KeyboardAvoiding, Text } from "@lumioui/ui";
import { useHaptic, useTheme } from "@lumioui/core";
import { AppleLogo, BLOCK_STAGGER, GoogleLogo, HeroGradient, Mount, World } from "../shared";

export interface LoginScreenProps {
  onLogin?: (email: string, password: string, remember: boolean) => void;
  onSignUp?: () => void;
  onForgotPassword?: () => void;
  onSocialLogin?: (provider: "apple" | "google") => void;
  logo?: React.ReactNode;
  /** Prefilled field values for demos/previews — production hosts omit
   *  these so the form starts empty. */
  defaultEmail?: string;
  defaultPassword?: string;
  defaultRemember?: boolean;
}

interface FieldErrors {
  email?: string;
  password?: string;
}

/** Login screen — icon-leading email/password fields, social providers,
 *  remember me, and a staggered mount entrance. */
export function LoginScreen({ onLogin, onSignUp, onForgotPassword, onSocialLogin, logo, defaultEmail, defaultPassword, defaultRemember }: LoginScreenProps) {
  const haptics = useHaptic();
  const insets = useSafeAreaInsets();
  const { colors } = useTheme();
  const passwordRef = useRef<TextInput>(null);
  const [email, setEmail] = useState(defaultEmail ?? "");
  const [password, setPassword] = useState(defaultPassword ?? "");
  const [showPassword, setShowPassword] = useState(false);
  const [remember, setRemember] = useState(defaultRemember ?? false);
  const [loading, setLoading] = useState(false);
  const [errors, setErrors] = useState<FieldErrors>({});

  const canSubmit = email.trim().length > 0 && password.length > 0;

  const submit = () => {
    const e: FieldErrors = {};
    if (!email.includes("@")) e.email = "Enter a valid email address";
    if (password.length < 6) e.password = "Password must be at least 6 characters";
    setErrors(e);
    if (Object.keys(e).length) {
      haptics.error();
      return;
    }
    setLoading(true);
    onLogin?.(email, password, remember);
    setTimeout(() => setLoading(false), 800);
  };

  return (
    <World primary="#65A30D">
    <View className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]">
      {/* Ambient color field behind the form — glass surfaces need something
          to refract, so the brand hue bleeds down past the hero. */}
      <KeyboardAvoiding>
        <ScrollView
          showsVerticalScrollIndicator={false}
          keyboardShouldPersistTaps="handled"
          keyboardDismissMode="interactive"
          contentContainerClassName="flex-grow px-6"
          contentContainerStyle={{ paddingBottom: insets.bottom + 24 }}
        >
          {/* Brand hero — deep indigo wash bleeding to the top edge (the
              -mx-6 cancels the scroll padding). Signature moment: raw hex
              is art direction here, like StoryChrome. */}
          <Mount delay={0} className="-mx-6">
            <View className="overflow-hidden bg-[#1B1B26] px-6 pb-7" style={{ paddingTop: insets.top + 28 }}>
              <HeroGradient from="#23232E" to="#14141A" />
              <View className="gap-4">
                {logo ?? (
                  <Glass interactive effect="clear" className="h-12 w-12 items-center justify-center rounded-2xl">
                    <Icon name="zap" size="lg" color="#FFFFFF" />
                  </Glass>
                )}
                <View className="gap-2">
                  <Text className="text-[34px] font-bold leading-[38px] tracking-tight text-white">
                    Design systems{"\n"}for touch.
                  </Text>
                  <Text className="max-w-[240px] text-[15px] leading-relaxed text-white/70">
                    Springs, haptics, and choreography — shipped standard,
                    not bolted on.
                  </Text>
                </View>
                <View className="flex-row gap-2 pt-1">
                  {["Springs", "Haptics", "Motion"].map((chip) => (
                    <Glass interactive key={chip} effect="clear" sheen={0.6} className="rounded-full px-3 py-1.5">
                      <Text className="text-xs font-medium text-white">{chip}</Text>
                    </Glass>
                  ))}
                </View>
              </View>
            </View>
          </Mount>

          {/* Form — fields stagger in under the hero */}
          <View className="mt-8 gap-4">
            <Mount delay={0}>
              <Text variant="h2" className="tracking-tight">Welcome <Text className="italic text-[#65A30D]">back</Text></Text>
            </Mount>
            <Mount delay={BLOCK_STAGGER}>
              <Input
                glass
                label="Email"
                placeholder="you@example.com"
                leftIcon={<Icon name="mail" size="sm" color="mutedForeground" />}
                value={email}
                onChangeText={(v) => {
                  setEmail(v);
                  if (errors.email) setErrors((e) => ({ ...e, email: undefined }));
                }}
                keyboardType="email-address"
                autoCapitalize="none"
                autoComplete="email"
                textContentType="emailAddress"
                returnKeyType="next"
                onSubmitEditing={() => passwordRef.current?.focus()}
                error={errors.email}
              />
            </Mount>
            <Mount delay={BLOCK_STAGGER * 2}>
              <View>
                <Input
                  glass
                  ref={passwordRef}
                  label="Password"
                  placeholder="Your password"
                  leftIcon={<Icon name="lock" size="sm" color="mutedForeground" />}
                  value={password}
                  onChangeText={(v) => {
                    setPassword(v);
                    if (errors.password) setErrors((e) => ({ ...e, password: undefined }));
                  }}
                  secureTextEntry={!showPassword}
                  autoComplete="password"
                  textContentType="password"
                  returnKeyType="done"
                  onSubmitEditing={submit}
                  error={errors.password}
                  className="pr-11"
                />
                {/* Visibility toggle — overlays the field's right edge; pr-11 above reserves the space. */}
                <Pressable
                  onPress={() => {
                    haptics.selection();
                    setShowPassword((v) => !v);
                  }}
                  accessibilityRole="button"
                  accessibilityLabel={showPassword ? "Hide password" : "Show password"}
                  accessibilityState={{ selected: showPassword }}
                  hitSlop={4}
                  className="absolute right-1 top-7 h-11 w-11 items-center justify-center rounded-full"
                >
                  <Icon name={showPassword ? "eye-off" : "eye"} size="sm" color="mutedForeground" />
                </Pressable>
              </View>
            </Mount>

            <Mount delay={BLOCK_STAGGER * 3}>
              <View className="flex-row items-center justify-between">
                {/* Whole row toggles — the checkbox inside is visual-only for a11y,
                    so there's a single 44pt "Remember me" checkbox stop. */}
                <Pressable
                  onPress={() => {
                    haptics.selection();
                    setRemember((v) => !v);
                  }}
                  accessibilityRole="checkbox"
                  accessibilityState={{ checked: remember }}
                  accessibilityLabel="Remember me"
                  hitSlop={8}
                  className="min-h-11 flex-row items-center gap-2"
                >
                  <Checkbox
                    checked={remember}
                    onCheckedChange={setRemember}
                    accessible={false}
                    accessibilityElementsHidden
                    importantForAccessibility="no"
                  />
                  <Text variant="small" className="text-muted-foreground">
                    Remember me
                  </Text>
                </Pressable>
                <Pressable
                  onPress={() => {
                    haptics.light();
                    onForgotPassword?.();
                  }}
                  accessibilityRole="link"
                  accessibilityLabel="Forgot password"
                  hitSlop={8}
                  className="min-h-11 justify-center"
                >
                  <Text variant="small" className="text-[#65A30D]">
                    Forgot password?
                  </Text>
                </Pressable>
              </View>
            </Mount>

            <Mount delay={BLOCK_STAGGER * 4}>
              <Button
                size="lg"
                loading={loading}
                disabled={!canSubmit}
                onPress={submit}
                haptic="medium"
                className="bg-[#65A30D] w-full"
              >
                Sign in
              </Button>
              {/* Returning-user fast path — biometric sign-in rides under the
                  primary button on every banking app. */}
              <Pressable
                onPress={() => {
                  haptics.medium();
                  onLogin?.("", "", true);
                }}
                accessibilityRole="button"
                accessibilityLabel="Sign in with Face ID"
                hitSlop={8}
                className="mt-1 min-h-11 flex-row items-center justify-center gap-2 self-center overflow-hidden rounded-full px-5"
              >
                <Glass interactive effect="clear" sheen={0.5} className="absolute inset-0 rounded-full" pointerEvents="none" />
                <Icon name="scan-face" size="sm" color="#65A30D" />
                <Text variant="small" className="font-medium text-[#65A30D]">
                  Sign in with Face ID
                </Text>
              </Pressable>
            </Mount>
          </View>

          {/* Divider — hairline with quiet caps label */}
          <Mount delay={BLOCK_STAGGER * 5} className="mt-9">
            <Divider
              label="or continue with"
              labelClassName="text-xs uppercase tracking-widest"
            />
          </Mount>

          {/* Social providers — liquid-glass pills with the real brand marks. */}
          <Mount delay={BLOCK_STAGGER * 6}>
            <View className="mt-7 flex-row gap-3">
              <Button
                variant="glass"
                className="flex-1"
                icon={<GoogleLogo />}
                onPress={() => onSocialLogin?.("google")}
                accessibilityLabel="Continue with Google"
              >
                Google
              </Button>
              <Button
                variant="glass"
                className="flex-1"
                icon={<AppleLogo color={colors.foreground} />}
                onPress={() => onSocialLogin?.("apple")}
                accessibilityLabel="Continue with Apple"
              >
                Apple
              </Button>
            </View>
          </Mount>

          {/* Footer — pinned to the bottom of the viewport */}
          <Mount delay={BLOCK_STAGGER * 7} className="mt-auto">
            <View className="items-center pt-8">
              <Pressable
                onPress={() => {
                  haptics.light();
                  onSignUp?.();
                }}
                className="min-h-11 items-center justify-center px-2"
                accessibilityRole="link"
                accessibilityLabel="Sign up for a new account"
                hitSlop={8}
              >
                <Text variant="muted">
                  Don&apos;t have an account?{" "}
                  <Text className="text-[#65A30D] font-semibold">Sign up</Text>
                </Text>
              </Pressable>
            </View>
          </Mount>
        </ScrollView>
      </KeyboardAvoiding>
    </View>
    </World>
  );
}
```

Screen: https://lumioui.dev/screens/auth-login
Pixel 10 · Android
9:41
Live preview — the real screen via Expo Web · open full ↗