Skip to main content

React Native Prompt: Text Input on iOS and Android

Learn how to build a React Native prompt with text input on iOS and Android, compare Alert.prompt(), packages, and custom modals for UX and validation.

·9 min read
React Native Prompt: Text Input on iOS and Android

You need a popup that accepts typed input, such as a name, access code, or password. React Native’s standard Alert.alert() has buttons for users to tap.

React Native’s Alert.prompt() adds a text field on iOS. React Native does not implement Alert.prompt() for Android, so Android releases need a separate input path.

This guide helps you choose the smallest reliable solution for your platforms, input rules, and submission flow.

React Native prompt: the short answer

React Native’s Alert.prompt() is its built-in iOS method for a native alert with a text field.

The right approach depends on whether the screen must behave the same on iOS and Android. Start with platform coverage, then decide how much control the prompt needs.

  • Native iOS prompt: Use Alert.prompt() when the flow only needs Apple’s native text-input alert.
  • Prompt package: Use a package when both platforms need a similar dialog and an added dependency fits the project.
  • Custom modal: Use a React Native Modal when layout, validation, or submission behavior must be controlled inside the app.

All three paths can collect text, but they differ in platform coverage, validation, layout, and dismissal behavior.

Does Alert.prompt() work on Android?

No. React Native’s Alert.prompt() API is available on iOS only, so Android needs a different text-input interface.

On Android, Alert.alert() has a title, message, and tappable buttons. The native Android alert does not include the editable field that appears in the iOS prompt.

Keep Alert.prompt() when the feature is intentionally iOS-only. In shared code, guard the call and route Android users to a package dialog or custom modal.

import React, { useState } from 'react';
import { Alert, Button, Platform, View } from 'react-native';
import { PromptDialog } from './PromptDialog';

export function NamePrompt() {
  const [promptVisible, setPromptVisible] = useState(false);

  const handleName = (name: string) => {
    console.log(name);
  };

  return (
    <View>
      <Button
        title="Edit name"
        onPress={() => {
          if (Platform.OS === 'ios') {
            Alert.prompt('Name', 'Enter your display name', handleName);
          } else {
            setPromptVisible(true);
          }
        }}
      />
      <PromptDialog
        visible={promptVisible}
        title="Name"
        message="Enter your display name"
        placeholder="e.g. Taylor"
        onCancel={() => setPromptVisible(false)}
        onSubmit={(name) => {
          handleName(name);
          setPromptVisible(false);
        }}
      />
    </View>
  );
}

Choose your implementation: native, package, or custom

Choose Alert.prompt() for a simple iOS-only field. Use a package for a shared dialog API, or a custom Modal when Android support, validation, async submission, or UI control matters.

Use these four criteria to confirm the fit before you commit:

  • Platform support: Use Alert.prompt() for iOS. If Android users also need to enter text, use a prompt package or custom Modal.
  • Visual control: Use the native prompt for standard iOS styling. Choose a package or custom Modal when the prompt must share one layout across platforms.
  • Maintenance: A package needs dependency, React Native, and native-build compatibility checks. A custom Modal leaves focus, keyboard, dismissal, and accessibility behavior to your app.
  • Submit behavior: The native prompt dismisses after a button tap, which makes inline validation and failed-request retries awkward. A custom Modal can stay open until submission succeeds.

Basic Alert.prompt() example on iOS

On iOS, Alert.prompt() shows a native alert with a text field. Pass a title, message, button array, and the plain-text type for a standard field.

import { Alert } from 'react-native';

export function showUsernamePrompt(onSubmit) {
  Alert.prompt(
    'Username',
    'Enter your new username below',
    [
      {
        text: 'Cancel',
        style: 'cancel',
      },
      {
        text: 'Submit',
        onPress: onSubmit,
      },
    ],
    'plain-text'
  );
}

The call uses four arguments:

  • Title: 'Username' names the prompt.
  • Message: 'Enter your new username below' tells the user what to enter.
  • Buttons: The array creates Cancel and Submit actions. The Submit handler receives the entered text as onSubmit’s argument.
  • Type: 'plain-text' sets a standard text field.

Customizing the prompt: buttons, secure input, and defaults

On iOS, Alert.prompt() lets you change buttons and text-field settings. Later arguments control the input type, initial value, and keyboard.

Buttons and callback behavior

Pass the buttons array as the third argument. Each button has a label and can run an onPress function; a confirm handler receives the typed text.

function showUsernamePrompt(saveUsername: (value: string) => void) {
  const buttons = [
    { text: 'Cancel', style: 'cancel' as const },
    {
      text: 'Submit',
      onPress: (value?: string) => saveUsername(value ?? ''),
    },
  ];

  Alert.prompt('Username', 'Enter a username', buttons);
}

Each button can include:

  • text for its label
  • onPress for its handler
  • style on iOS: default, cancel, or destructive

Secure text entry for passwords

Use secure-text as the fourth argument to mask password characters.

Alert.prompt('Password', 'Enter your password', handlePassword, 'secure-text');

Default values and keyboard type

Use the fifth argument to prefill the field and the sixth to choose the keyboard type.

Alert.prompt(
  'Email',
  'Confirm your email address',
  handleEmail,
  'plain-text',
  '[email protected]',
  'email-address'
);

For an iOS alert with username and password fields, React Native also provides the login-password input type. A full sign-in flow is usually clearer as a screen, sheet, or custom modal.

Dependency-free option: a custom Modal prompt

import React, { useCallback, useEffect, useRef, useState } from 'react';
import {
  ActivityIndicator,
  KeyboardAvoidingView,
  Modal,
  Platform,
  Pressable,
  StyleSheet,
  Text,
  TextInput,
  type TextInputProps,
  View,
} from 'react-native';

type PromptDialogProps = {
  visible: boolean;
  title: string;
  message?: string;
  placeholder?: string;
  defaultValue?: string;
  cancelText?: string;
  submitText?: string;
  keyboardType?: TextInputProps['keyboardType'];
  secureTextEntry?: boolean;
  validate?: (value: string) => string | null;
  onCancel: () => void;
  onSubmit: (value: string) => void | Promise<void>;
};

export function PromptDialog({
  visible,
  title,
  message,
  placeholder,
  defaultValue = '',
  cancelText = 'Cancel',
  submitText = 'Save',
  keyboardType = 'default',
  secureTextEntry = false,
  validate,
  onCancel,
  onSubmit,
}: PromptDialogProps) {
  const [value, setValue] = useState(defaultValue);
  const [error, setError] = useState<string | null>(null);
  const [submitting, setSubmitting] = useState(false);
  const submitLock = useRef(false);

  useEffect(() => {
    if (!visible) return;
    setValue(defaultValue);
    setError(null);
    setSubmitting(false);
    submitLock.current = false;
  }, [defaultValue, visible]);

  const handleSubmit = useCallback(async () => {
    if (submitLock.current) return;
    const validationError = validate?.(value) ?? null;
    if (validationError) {
      setError(validationError);
      return;
    }

    submitLock.current = true;
    setSubmitting(true);
    setError(null);
    try {
      await onSubmit(value);
    } catch (caughtError) {
      const message =
        caughtError instanceof Error
          ? caughtError.message
          : 'Could not submit. Try again.';
      setError(message);
    } finally {
      submitLock.current = false;
      setSubmitting(false);
    }
  }, [onSubmit, validate, value]);

  const handleCancel = () => {
    if (!submitting) onCancel();
  };

  return (
    <Modal
      visible={visible}
      transparent
      animationType="fade"
      onRequestClose={handleCancel}
    >
      <KeyboardAvoidingView
        style={styles.overlay}
        behavior={Platform.OS === 'ios' ? 'padding' : undefined}
      >
        <View accessibilityViewIsModal style={styles.card}>
          <Text style={styles.title}>{title}</Text>
          {message ? <Text style={styles.message}>{message}</Text> : null}
          <TextInput
            accessibilityLabel={title}
            autoFocus
            editable={!submitting}
            keyboardType={keyboardType}
            placeholder={placeholder}
            onChangeText={(nextValue) => {
              setValue(nextValue);
              if (error) setError(null);
            }}
            onSubmitEditing={handleSubmit}
            returnKeyType="done"
            secureTextEntry={secureTextEntry}
            style={[styles.input, error ? styles.inputError : null]}
            value={value}
          />
          {error ? (
            <Text accessibilityLiveRegion="polite" style={styles.error}>
              {error}
            </Text>
          ) : null}
          <View style={styles.actions}>
            <Pressable
              accessibilityRole="button"
              accessibilityState={{ disabled: submitting }}
              disabled={submitting}
              onPress={handleCancel}
              style={styles.button}
            >
              <Text style={styles.cancelLabel}>{cancelText}</Text>
            </Pressable>
            <Pressable
              accessibilityRole="button"
              accessibilityState={{ busy: submitting, disabled: submitting }}
              disabled={submitting}
              onPress={handleSubmit}
              style={[styles.button, styles.submitButton]}
            >
              {submitting ? (
                <ActivityIndicator color="#fff" />
              ) : (
                <Text style={styles.submitLabel}>{submitText}</Text>
              )}
            </Pressable>
          </View>
        </View>
      </KeyboardAvoidingView>
    </Modal>
  );
}

type ProfileNameEditorProps = {
  saveDisplayName: (name: string) => Promise<void>;
};

export function ProfileNameEditor({
  saveDisplayName,
}: ProfileNameEditorProps) {
  const [promptVisible, setPromptVisible] = useState(false);

  return (
    <>
      <Pressable onPress={() => setPromptVisible(true)}>
        <Text>Edit display name</Text>
      </Pressable>
      <PromptDialog
        visible={promptVisible}
        title="Display name"
        message="Enter the name shown on your profile."
        placeholder="e.g. Taylor"
        cancelText="Not now"
        submitText="Save"
        validate={(name) =>
          name.trim() ? null : 'Display name is required.'
        }
        onCancel={() => setPromptVisible(false)}
        onSubmit={async (name) => {
          await saveDisplayName(name.trim());
          setPromptVisible(false);
        }}
      />
    </>
  );
}

const styles = StyleSheet.create({
  overlay: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    padding: 24,
    backgroundColor: 'rgba(0, 0, 0, 0.45)',
  },
  card: {
    width: '100%',
    maxWidth: 420,
    padding: 20,
    borderRadius: 16,
    backgroundColor: '#fff',
  },
  title: { fontSize: 20, fontWeight: '700', color: '#111827' },
  message: { marginTop: 8, fontSize: 15, color: '#4b5563' },
  input: {
    marginTop: 16,
    paddingHorizontal: 12,
    paddingVertical: 10,
    borderWidth: 1,
    borderColor: '#9ca3af',
    borderRadius: 8,
    fontSize: 16,
    color: '#111827',
  },
  inputError: { borderColor: '#dc2626' },
  error: { marginTop: 8, color: '#dc2626' },
  actions: {
    flexDirection: 'row',
    justifyContent: 'flex-end',
    gap: 8,
    marginTop: 20,
  },
  button: {
    minWidth: 88,
    minHeight: 44,
    alignItems: 'center',
    justifyContent: 'center',
    paddingHorizontal: 16,
    borderRadius: 8,
  },
  submitButton: { backgroundColor: '#2563eb' },
  cancelLabel: { color: '#374151', fontWeight: '600' },
  submitLabel: { color: '#fff', fontWeight: '700' },
});

Building a reusable PromptDisalog component

PromptDialog is a controlled Modal. Its props control visibility and input behavior, while local TextInput state resets when the dialog opens.

accessibilityRole="button"

The component handles platform behavior in four parts:

  • Modal uses a transparent overlay and centered card on both platforms.
  • KeyboardAvoidingView adds iOS padding when the keyboard opens. Test Android with your app’s window-soft-input configuration, especially on smaller screens.
  • autoFocus opens the keyboard, and onSubmitEditing routes the return key through the same submit handler as the button.
  • placeholder, keyboardType, and secureTextEntry pass directly to TextInput for email, numeric, or password prompts.

Before shipping, test screen-reader labels, return focus to the trigger after closing, and decide whether backdrop taps should dismiss the dialog. If your React Native version does not support gap, use margins between buttons.

Adding validation and async submit handling

Validate before closing the Modal. Show an inline error for invalid values and disable buttons while onSubmit is pending.

The validator receives the controlled TextInput value. The caller decides whether to normalize it; the example uses trim() for validation and before saving.

Editing clears an old error. The next button press or return-key event sends the updated value to handleSubmit for validation.

The ref lock prevents two rapid events from starting duplicate requests before React updates submitting.

The parent controls visibility. ProfileNameEditor closes the dialog after saveDisplayName resolves; if it rejects, the controls return and the error appears in the existing error slot.

Which prompt approach fits your app?

Alert.prompt() fits simple iOS input. Use a package when a shared imperative prompt is enough. Choose a custom Modal for consistent cross-platform UI, validation, or async submission.

ApproachPlatform fitChoose it whenMain tradeoff
Alert.prompt()iOS onlyA native system dialog and callback are enoughIt cannot remain open to show inline validation or a failed request
Prompt packageiOS and AndroidYou want one prompt API and accept a dependencyAndroid behavior needs separate testing
Custom ModaliOS and AndroidYou need custom layout, extra fields, inline errors, or async submitYou own the component code and accessibility checks

For one simple iOS value, stay native. If the flow must survive validation or request failures on both platforms, a custom Modal is the most predictable option.

Common prompt errors and how to fix them

Common prompt bugs involve Android support, button callbacks, and Modal input state.

  • No input appears on Android: Alert.prompt() is unsupported there. Guard the iOS call with Platform.OS === 'ios', then open a verified package dialog or a Modal with TextInput.
  • Buttons dismiss the prompt but run the wrong action: Give Cancel and Submit separate onPress handlers. On iOS, read the typed string from the Submit callback; the cancel handler should dismiss without processing a value.
  • Secure entry or the wrong keyboard appears: The secure-text input type and prompt-level keyboard setting belong to iOS Alert.prompt(). In a custom dialog, set secureTextEntry and keyboardType on TextInput, and pass defaultValue as a string.
  • A custom prompt loses text or submits twice: Keep the field controlled, and disable Submit while the request is pending. Use a screen, sheet, or full form for multiple fields and detailed validation.

Build and publish the prompt flow with Bilt

If you want the input flow without maintaining dialog code and deployment yourself, describe it in Bilt. Bilt generates a real React Native app for iOS and Android.

Bilt app creation prompt for describing a React Native input flow
Bilt app creation prompt for describing a React Native input flow

Include the copy, field types, validation, and each button action. A single iOS value may fit Alert.prompt(), while a cross-platform sign-in flow needs multiple fields and validation that Bilt can build into the app.

Sample prompt

Build a form with email and secure-password fields. Label the buttons “Sign in” and “Cancel,” and show a validation message when either field is empty.

Then work through the generated flow:

  1. Preview it: Open the live iOS or Android preview and test the fields, keyboard, validation, and button actions.
  2. Check a real device: Use the QR code to test typing and screen behavior on your phone.
  3. Refine in chat: Ask the Bilt agent to change the copy, field rules, or button actions.
  4. Keep or export the code: Continue refining the project in Bilt, or export the React Native code when you need to work with a developer.
  5. Test and publish: Test the iOS build through TestFlight before release, then use Bilt’s publishing workflow to submit to the Apple App Store and Google Play Console.
Bilt workflow from app preview and device testing through refinement, export, and publishing
Bilt workflow from app preview and device testing through refinement, export, and publishing
  1. Once the flow works, keep the same Bilt project through store submission. Check keyboard behavior, validation, loading states, icons, store details, and release builds for both platforms before you submit.

Bilt keeps preview, device testing, refinement, and publishing in one project. You can test the generated flow on a device, change it in chat, then prepare builds for each store.

Video

Bilt turns your description into a real native app you can preview, refine, and publish on iOS and Android.

Bilt’s free backend can handle authentication and login flows when your app needs them, with no code required.

Start building free. No credit card required.

FAQs

How do you show a prompt-style input on Android?

React Native core does not support Alert.prompt() on Android. Open a custom Modal for validation or custom UI, or use a verified dialog package when you need an imperative prompt API.

Keep the platform branch in one shared helper. It can call Alert.prompt() on iOS and open the Android implementation through the same app-level function.

Can you customize how a React Native alert looks?

React Native core alerts follow operating-system styling, so you cannot restyle the dialog itself. You can still configure its title, message, buttons, and supported input options.

A custom dialog can match the app's colors, spacing, typography, and button layout. The tradeoff is ownership: your component must also handle focus, keyboard avoidance, accessibility labels, and dismissal behavior.

Does Alert.prompt() work in Expo apps?

Yes, on iOS. Expo uses React Native’s core Alert.prompt() API, including in Expo Go; Android still needs a custom Modal or another Android input interface.

A custom Modal works in Expo Go because it uses React Native components. Use that approach when the same input flow must work on iOS and Android.