Skip to content
All components

Free reference

React Native Password Input

A React Native password input is a TextInput with secureTextEntry plus a show and hide toggle, and the autofill props that let password managers fill and save it.

Installation

Reanimated is only there for the strength bar. Drop the meter and this is core React Native with no dependencies.

npx expo install react-native-reanimated react-native-worklets

Usage

Paste this into components/PasswordInput.tsx. Use mode="login" on a sign-in screen and mode="new" when someone is choosing a password, because password managers treat the two differently.

// PasswordInput.tsx
import { useState, type ComponentProps } from 'react';
import { Pressable, StyleSheet, Text, TextInput, View } from 'react-native';
import Animated, { useAnimatedStyle, withTiming } from 'react-native-reanimated';

type PasswordInputProps = Omit<
  ComponentProps<typeof TextInput>,
  'value' | 'onChangeText' | 'style' | 'secureTextEntry'
> & {
  value: string;
  onChangeText: (text: string) => void;
  mode?: 'login' | 'new';
};

const AUTOFILL = {
  login: { autoComplete: 'current-password', textContentType: 'password' },
  new: {
    autoComplete: 'new-password',
    textContentType: 'newPassword',
    passwordRules: 'minlength: 12; required: lower; required: upper; required: digit;',
  },
} as const;

const LEVELS = [
  { label: 'Too short', color: '#D92D20' },
  { label: 'Weak', color: '#D92D20' },
  { label: 'Okay', color: '#B54708' },
  { label: 'Good', color: '#067647' },
  { label: 'Strong', color: '#067647' },
];

function scorePassword(value: string): number {
  if (value.length < 8) return 0;
  let score = 1;
  if (value.length >= 12) score += 1;
  if (/[a-z]/.test(value) && /[A-Z]/.test(value)) score += 1;
  if (/\d/.test(value) && /[^A-Za-z0-9]/.test(value)) score += 1;
  return score;
}

export function PasswordInput({
  value,
  onChangeText,
  mode = 'login',
  ...inputProps
}: PasswordInputProps) {
  const [visible, setVisible] = useState(false);
  const showMeter = mode === 'new' && value.length > 0;
  const score = scorePassword(value);
  const level = LEVELS[score];

  const barStyle = useAnimatedStyle(() => ({
    width: withTiming(`${(Math.max(score, 1) / 4) * 100}%`, { duration: 180 }),
    backgroundColor: withTiming(level.color, { duration: 180 }),
  }));

  return (
    <View style={styles.root}>
      <View style={styles.box}>
        <TextInput
          {...AUTOFILL[mode]}
          {...inputProps}
          value={value}
          onChangeText={onChangeText}
          secureTextEntry={!visible}
          autoCapitalize="none"
          autoCorrect={false}
          spellCheck={false}
          accessibilityLabel={mode === 'new' ? 'New password' : 'Password'}
          style={styles.input}
        />
        <Pressable
          onPress={() => setVisible((v) => !v)}
          hitSlop={12}
          accessibilityRole="button"
          accessibilityLabel={visible ? 'Hide password' : 'Show password'}
        >
          <Text style={styles.toggle}>{visible ? 'Hide' : 'Show'}</Text>
        </Pressable>
      </View>

      {showMeter ? (
        <View accessible accessibilityLabel={`Password strength: ${level.label}`}>
          <View style={styles.track}>
            <Animated.View style={[styles.bar, barStyle]} />
          </View>
          <Text style={[styles.level, { color: level.color }]}>{level.label}</Text>
        </View>
      ) : null}
    </View>
  );
}

const styles = StyleSheet.create({
  root: { gap: 8 },
  box: {
    height: 52,
    flexDirection: 'row',
    alignItems: 'center',
    gap: 12,
    borderRadius: 12,
    borderWidth: 1.5,
    borderColor: '#E4E4E7',
    backgroundColor: '#FFFFFF',
    paddingHorizontal: 14,
  },
  input: {
    flex: 1,
    height: '100%',
    padding: 0,
    fontSize: 16,
    color: '#18181B',
    includeFontPadding: false,
  },
  toggle: { fontSize: 14, fontWeight: '600', color: '#18181B' },
  track: {
    height: 4,
    borderRadius: 2,
    overflow: 'hidden',
    backgroundColor: '#F4F4F5',
  },
  bar: { height: '100%', borderRadius: 2 },
  level: { marginTop: 4, fontSize: 13, fontWeight: '500' },
});

How it works

secureTextEntry does the masking and it is driven by one boolean, so the toggle is a Pressable that flips it. A tap on the Pressable does not take focus from the input, so the keyboard stays up while someone checks what they typed.

The AUTOFILL table is the part most tutorials leave out. textContentType tells iOS whether this is an existing password or a new one, autoComplete does the same on Android, and passwordRules tells iOS what a generated password has to contain. Without them the field still masks text, but the password manager stays quiet.

The strength bar is a rough hint from length and character variety, and that is all it should claim to be. Enforce your real password policy on the server, and if the meter matters to you, @zxcvbn-ts/core estimates strength from actual guessing patterns.

Gotchas

iOS clears the field when you start typing again

This is standard UIKit behaviour for secure fields: when a secure input regains focus and the user types, the old value is wiped and replaced by the new character. It looks like a bug and people report it as one, but it is what native iOS apps do too. Don't try to fight it unless your design really needs you to.

Autofill needs your domain linked to your app

The props are only half of it. For iOS to offer the saved password for your site, the app needs an Associated Domains entry of the form webcredentials:yourdomain.com, plus an apple-app-site-association file on that domain. In Expo that goes in ios.associatedDomains in your app config. Android's equivalent is a Digital Asset Links file.

Pair it with a username field

Password managers save the pair, so the field just before this one should be a text field with textContentType="username" or "emailAddress". A password field alone often produces no save prompt at all.

secureTextEntry does not work with multiline

React Native's docs are explicit about this: a secure field has to be single line. If masking seems to have stopped working, look for a stray multiline prop, which turns the field into a text area.

Accessibility

The toggle needs accessibilityRole="button" and a label that changes with its state, otherwise VoiceOver just reads the word on screen with no context. The meter is grouped into one element so it is announced as a single sentence instead of a stray bar and word.

How do I hide password text in React Native?

Set secureTextEntry on TextInput. It replaces each character with a dot on both iOS and Android. It only works on single-line inputs, so it can't be combined with multiline.

How do I show and hide a password in React Native?

Keep a visible boolean in state, pass secureTextEntry={!visible} to the input, and flip it from a Pressable beside the field. Give that button a label that changes between "Show password" and "Hide password" so screen readers report the state.

How do I enable password autofill in React Native?

Set autoComplete="current-password" with textContentType="password" on sign-in, or autoComplete="new-password" with textContentType="newPassword" on sign-up. Then link your domain to your app with Associated Domains on iOS and Digital Asset Links on Android so saved credentials for your site are offered.

Why does my password field clear on iOS?

iOS secure text fields clear their contents when the user types after the field regains focus. It is native behaviour rather than a React Native bug, and standard iOS apps do the same thing.

After the password, most sign-in flows ask for a code, which is an OTP input. If you want that step animated and finished, the OTP code input drop is ready to paste in.

Depends onreact-native-reanimatedreact-native-worklets

Every component on this page is free to copy. Motionary sells the polished, production versions over at the catalog.