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

Password Reset Screen

New-password form — segmented strength meter, live requirements checklist, confirm-match validation, and reset CTA

pro Expo Go ✓ iosandroid gestures: tap · scroll haptics: light · selection · medium · success · error
$ npx lumioui add auth-password-reset
import { PasswordResetScreen } from "@/screens/auth/password-reset";
Registry dependencies5
Native packages installed6
@lumioui/ui@latest@lumioui/core@latestreact-native-safe-area-context@*react-native-reanimated@*react-native-svg@*nativewind@*
auth/password-reset.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, Icon, Input, Text, KeyboardAvoiding } from "@lumioui/ui";
import { useHaptic } from "@lumioui/core";
import { Mount, World } from "../shared";

export interface PasswordResetScreenProps {
  /** The account being reset — shown under the title. */
  email?: string;
  onReset?: (password: string) => void;
  onBack?: () => void;
}

interface Rule {
  key: string;
  label: string;
  test: (password: string) => boolean;
}

const RULES: Rule[] = [
  { key: "length", label: "At least 8 characters", test: (p) => p.length >= 8 },
  { key: "case", label: "Upper and lowercase letters", test: (p) => /[a-z]/.test(p) && /[A-Z]/.test(p) },
  { key: "number", label: "A number", test: (p) => /\d/.test(p) },
  { key: "symbol", label: "A symbol", test: (p) => /[^a-zA-Z0-9]/.test(p) },
];

const STRENGTH_LABELS = ["Too weak", "Weak", "Fair", "Strong", "Very strong"];

/** Password reset — new-password form with a segmented strength meter and a
 *  live requirements checklist that ticks off as the user types. */
export function PasswordResetScreen({
  email = "amara.chen@lumio.dev",
  onReset,
  onBack,
}: PasswordResetScreenProps) {
  const haptics = useHaptic();
  const insets = useSafeAreaInsets();
  const confirmRef = useRef<TextInput>(null);
  const [password, setPassword] = useState("Vanta#2026lumio");
  const [confirm, setConfirm] = useState("Vanta#2026lumio");
  const [showPassword, setShowPassword] = useState(false);
  const [loading, setLoading] = useState(false);
  const [mismatch, setMismatch] = useState(false);

  const passed = RULES.filter((r) => r.test(password)).length;
  const score = password.length === 0 ? 0 : passed;
  const allPassed = passed === RULES.length;
  const matches = confirm.length === 0 || confirm === password;
  const canSubmit = allPassed && confirm === password && password.length > 0;

  const submit = () => {
    if (confirm !== password) {
      setMismatch(true);
      haptics.error();
      return;
    }
    setMismatch(false);
    setLoading(true);
    haptics.medium();
    onReset?.(password);
    setTimeout(() => {
      setLoading(false);
      haptics.success();
    }, 800);
  };

  return (
    <World primary="#65A30D">
    <View className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]">
      {/* Top bar — back affordance clears the status bar */}
      <View className="px-2" style={{ paddingTop: insets.top + 4 }}>
        <Pressable
          onPress={() => {
            haptics.light();
            onBack?.();
          }}
          accessibilityRole="button"
          accessibilityLabel="Go back"
          hitSlop={8}
          className="h-11 flex-row items-center gap-1 self-start px-2"
        >
          <Icon name="chevron-left" size="sm" color="#65A30D" />
          <Text className="text-base text-[#65A30D]">Back</Text>
        </Pressable>
      </View>

      <KeyboardAvoiding>
        <ScrollView
          showsVerticalScrollIndicator={false}
          keyboardShouldPersistTaps="handled"
          keyboardDismissMode="interactive"
          contentContainerClassName="flex-grow px-6"
          contentContainerStyle={{ paddingTop: 8, paddingBottom: insets.bottom + 24 }}
        >
          <Mount delay={0}>
            <View className="gap-3">
              <Text variant="h1" className="tracking-tight">
                Set a new <Text className="italic text-[#65A30D]">password</Text>
              </Text>
              <Text variant="muted" className="max-w-xs text-base leading-relaxed">
                For <Text className="font-semibold text-foreground">{email}</Text> —
                pick something you haven&apos;t used here before.
              </Text>
            </View>
          </Mount>

          <Mount delay={140}>
            <View className="mt-8 gap-4">
              <View>
                <Input
                  label="New password"
                  placeholder="New password"
                  leftIcon={<Icon name="lock" size="sm" color="mutedForeground" />}
                  value={password}
                  onChangeText={setPassword}
                  secureTextEntry={!showPassword}
                  autoComplete="new-password"
                  textContentType="newPassword"
                  returnKeyType="next"
                  onSubmitEditing={() => confirmRef.current?.focus()}
                  className="pr-11"
                  glass
                />
                <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>
              <Input
                ref={confirmRef}
                label="Confirm password"
                placeholder="Repeat it"
                leftIcon={<Icon name="lock" size="sm" color="mutedForeground" />}
                value={confirm}
                onChangeText={(v) => {
                  setConfirm(v);
                  if (mismatch) setMismatch(false);
                }}
                secureTextEntry={!showPassword}
                autoComplete="new-password"
                textContentType="newPassword"
                returnKeyType="done"
                onSubmitEditing={submit}
                error={mismatch || !matches ? "Passwords don't match" : undefined}
                glass
              />
            </View>
          </Mount>

          {/* Strength meter — the meter is the feedback, not decoration */}
          <Mount delay={220}>
            <View className="mt-6 gap-2">
              <View className="flex-row items-center justify-between">
                <Text variant="muted" className="text-xs">
                  Password strength
                </Text>
                <Text
                  variant="small"
                  className={
                    score >= 3
                      ? "text-xs font-semibold text-success"
                      : score >= 1
                        ? "text-xs font-semibold text-warning"
                        : "text-xs font-semibold text-muted-foreground"
                  }
                  accessibilityLiveRegion="polite"
                >
                  {STRENGTH_LABELS[Math.min(score, 4)]}
                </Text>
              </View>
              <View
                className="flex-row gap-1.5"
                accessibilityRole="progressbar"
                accessibilityLabel={`Password strength ${STRENGTH_LABELS[Math.min(score, 4)]}`}
                accessibilityValue={{ min: 0, max: 4, now: score }}
              >
                {[0, 1, 2, 3].map((seg) => (
                  <View
                    key={seg}
                    className={
                      seg < score
                        ? score >= 3
                          ? "h-1.5 flex-1 rounded-full bg-success"
                          : "h-1.5 flex-1 rounded-full bg-warning"
                        : "h-1.5 flex-1 rounded-full bg-muted"
                    }
                  />
                ))}
              </View>
              {/* Crack-time estimate — the concrete consequence users care
                  about, not an abstract score. */}
              <Text variant="muted" className="text-xs">
                {score >= 4
                  ? "Centuries to crack — excellent."
                  : score === 3
                    ? "~6 months to crack — good."
                    : score === 2
                      ? "~3 weeks to crack — add a symbol."
                      : score === 1
                        ? "Minutes to crack — too easy."
                        : "Instantly guessable."}
              </Text>
            </View>
          </Mount>

          {/* Requirements — a live checklist that ticks off while typing */}
          <Mount delay={280}>
            <View className="mt-5 gap-2.5 overflow-hidden rounded-2xl bg-white p-4 dark:bg-[#1A1A20]">
              {RULES.map((rule) => {
                const ok = rule.test(password);
                return (
                  <View key={rule.key} className="flex-row items-center gap-2.5">
                    <Icon
                      name={ok ? "check-circle" : "x"}
                      size="sm"
                      color={ok ? "success" : "mutedForeground"}
                    />
                    <Text
                      variant="small"
                      className={ok ? "text-foreground" : "text-muted-foreground"}
                    >
                      {rule.label}
                    </Text>
                  </View>
                );
              })}
            </View>
          </Mount>

          <Mount delay={340} className="mt-auto">
            <View className="pt-8">
              <Button
                size="lg"
                loading={loading}
                disabled={!canSubmit}
                onPress={submit}
                haptic={false}
                className="bg-[#65A30D] w-full"
                accessibilityLabel="Reset password"
              >
                Reset password
              </Button>
            </View>
          </Mount>
        </ScrollView>
      </KeyboardAvoiding>
    </View>
    </World>
  );
}
PasswordResetScreen.displayName = "PasswordResetScreen";
agent prompt — paste into Claude Code, Cursor, Devin…
Recreate the "Password Reset Screen" screen in React Native using LumioUI.

Goal: New-password form — segmented strength meter, live requirements checklist, confirm-match validation, and reset CTA 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-password-reset
```

Or pull just the components: `npx lumioui add button 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>` ×1

### `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

### `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/password-reset.tsx` exporting PasswordResetScreen. 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, Icon, Input, Text, KeyboardAvoiding } from "@lumioui/ui";
import { useHaptic } from "@lumioui/core";
import { Mount, World } from "../shared";

export interface PasswordResetScreenProps {
  /** The account being reset — shown under the title. */
  email?: string;
  onReset?: (password: string) => void;
  onBack?: () => void;
}

interface Rule {
  key: string;
  label: string;
  test: (password: string) => boolean;
}

const RULES: Rule[] = [
  { key: "length", label: "At least 8 characters", test: (p) => p.length >= 8 },
  { key: "case", label: "Upper and lowercase letters", test: (p) => /[a-z]/.test(p) && /[A-Z]/.test(p) },
  { key: "number", label: "A number", test: (p) => /\d/.test(p) },
  { key: "symbol", label: "A symbol", test: (p) => /[^a-zA-Z0-9]/.test(p) },
];

const STRENGTH_LABELS = ["Too weak", "Weak", "Fair", "Strong", "Very strong"];

/** Password reset — new-password form with a segmented strength meter and a
 *  live requirements checklist that ticks off as the user types. */
export function PasswordResetScreen({
  email = "amara.chen@lumio.dev",
  onReset,
  onBack,
}: PasswordResetScreenProps) {
  const haptics = useHaptic();
  const insets = useSafeAreaInsets();
  const confirmRef = useRef<TextInput>(null);
  const [password, setPassword] = useState("Vanta#2026lumio");
  const [confirm, setConfirm] = useState("Vanta#2026lumio");
  const [showPassword, setShowPassword] = useState(false);
  const [loading, setLoading] = useState(false);
  const [mismatch, setMismatch] = useState(false);

  const passed = RULES.filter((r) => r.test(password)).length;
  const score = password.length === 0 ? 0 : passed;
  const allPassed = passed === RULES.length;
  const matches = confirm.length === 0 || confirm === password;
  const canSubmit = allPassed && confirm === password && password.length > 0;

  const submit = () => {
    if (confirm !== password) {
      setMismatch(true);
      haptics.error();
      return;
    }
    setMismatch(false);
    setLoading(true);
    haptics.medium();
    onReset?.(password);
    setTimeout(() => {
      setLoading(false);
      haptics.success();
    }, 800);
  };

  return (
    <World primary="#65A30D">
    <View className="flex-1 bg-[#F4F5F9] dark:bg-[#111114]">
      {/* Top bar — back affordance clears the status bar */}
      <View className="px-2" style={{ paddingTop: insets.top + 4 }}>
        <Pressable
          onPress={() => {
            haptics.light();
            onBack?.();
          }}
          accessibilityRole="button"
          accessibilityLabel="Go back"
          hitSlop={8}
          className="h-11 flex-row items-center gap-1 self-start px-2"
        >
          <Icon name="chevron-left" size="sm" color="#65A30D" />
          <Text className="text-base text-[#65A30D]">Back</Text>
        </Pressable>
      </View>

      <KeyboardAvoiding>
        <ScrollView
          showsVerticalScrollIndicator={false}
          keyboardShouldPersistTaps="handled"
          keyboardDismissMode="interactive"
          contentContainerClassName="flex-grow px-6"
          contentContainerStyle={{ paddingTop: 8, paddingBottom: insets.bottom + 24 }}
        >
          <Mount delay={0}>
            <View className="gap-3">
              <Text variant="h1" className="tracking-tight">
                Set a new <Text className="italic text-[#65A30D]">password</Text>
              </Text>
              <Text variant="muted" className="max-w-xs text-base leading-relaxed">
                For <Text className="font-semibold text-foreground">{email}</Text> —
                pick something you haven&apos;t used here before.
              </Text>
            </View>
          </Mount>

          <Mount delay={140}>
            <View className="mt-8 gap-4">
              <View>
                <Input
                  label="New password"
                  placeholder="New password"
                  leftIcon={<Icon name="lock" size="sm" color="mutedForeground" />}
                  value={password}
                  onChangeText={setPassword}
                  secureTextEntry={!showPassword}
                  autoComplete="new-password"
                  textContentType="newPassword"
                  returnKeyType="next"
                  onSubmitEditing={() => confirmRef.current?.focus()}
                  className="pr-11"
                  glass
                />
                <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>
              <Input
                ref={confirmRef}
                label="Confirm password"
                placeholder="Repeat it"
                leftIcon={<Icon name="lock" size="sm" color="mutedForeground" />}
                value={confirm}
                onChangeText={(v) => {
                  setConfirm(v);
                  if (mismatch) setMismatch(false);
                }}
                secureTextEntry={!showPassword}
                autoComplete="new-password"
                textContentType="newPassword"
                returnKeyType="done"
                onSubmitEditing={submit}
                error={mismatch || !matches ? "Passwords don't match" : undefined}
                glass
              />
            </View>
          </Mount>

          {/* Strength meter — the meter is the feedback, not decoration */}
          <Mount delay={220}>
            <View className="mt-6 gap-2">
              <View className="flex-row items-center justify-between">
                <Text variant="muted" className="text-xs">
                  Password strength
                </Text>
                <Text
                  variant="small"
                  className={
                    score >= 3
                      ? "text-xs font-semibold text-success"
                      : score >= 1
                        ? "text-xs font-semibold text-warning"
                        : "text-xs font-semibold text-muted-foreground"
                  }
                  accessibilityLiveRegion="polite"
                >
                  {STRENGTH_LABELS[Math.min(score, 4)]}
                </Text>
              </View>
              <View
                className="flex-row gap-1.5"
                accessibilityRole="progressbar"
                accessibilityLabel={`Password strength ${STRENGTH_LABELS[Math.min(score, 4)]}`}
                accessibilityValue={{ min: 0, max: 4, now: score }}
              >
                {[0, 1, 2, 3].map((seg) => (
                  <View
                    key={seg}
                    className={
                      seg < score
                        ? score >= 3
                          ? "h-1.5 flex-1 rounded-full bg-success"
                          : "h-1.5 flex-1 rounded-full bg-warning"
                        : "h-1.5 flex-1 rounded-full bg-muted"
                    }
                  />
                ))}
              </View>
              {/* Crack-time estimate — the concrete consequence users care
                  about, not an abstract score. */}
              <Text variant="muted" className="text-xs">
                {score >= 4
                  ? "Centuries to crack — excellent."
                  : score === 3
                    ? "~6 months to crack — good."
                    : score === 2
                      ? "~3 weeks to crack — add a symbol."
                      : score === 1
                        ? "Minutes to crack — too easy."
                        : "Instantly guessable."}
              </Text>
            </View>
          </Mount>

          {/* Requirements — a live checklist that ticks off while typing */}
          <Mount delay={280}>
            <View className="mt-5 gap-2.5 overflow-hidden rounded-2xl bg-white p-4 dark:bg-[#1A1A20]">
              {RULES.map((rule) => {
                const ok = rule.test(password);
                return (
                  <View key={rule.key} className="flex-row items-center gap-2.5">
                    <Icon
                      name={ok ? "check-circle" : "x"}
                      size="sm"
                      color={ok ? "success" : "mutedForeground"}
                    />
                    <Text
                      variant="small"
                      className={ok ? "text-foreground" : "text-muted-foreground"}
                    >
                      {rule.label}
                    </Text>
                  </View>
                );
              })}
            </View>
          </Mount>

          <Mount delay={340} className="mt-auto">
            <View className="pt-8">
              <Button
                size="lg"
                loading={loading}
                disabled={!canSubmit}
                onPress={submit}
                haptic={false}
                className="bg-[#65A30D] w-full"
                accessibilityLabel="Reset password"
              >
                Reset password
              </Button>
            </View>
          </Mount>
        </ScrollView>
      </KeyboardAvoiding>
    </View>
    </World>
  );
}
PasswordResetScreen.displayName = "PasswordResetScreen";
```

Screen: https://lumioui.dev/screens/auth-password-reset
iPhone 17 · iOS
9:41
Live preview — the real screen via Expo Web · open full ↗