ShipNative
PricingBlogFAQContact
Build my app
ShipNative
PricingBlogFAQContact
Build my app
GuideOctober 2026 · 8 min read · By Saurabh Singh

React Native Number Input: Keyboard, Decimals, Currency

React Native has no <input type="number">. What it has is a TextInput that always hands you a string, a keyboardType prop that only changes which keyboard pops up, and two platforms that disagree about where the decimal point and the minus sign live. This guide is the working version: the right props, a controlled input that survives partial values, a currency field that formats as you type without the cursor jumping, and the iOS number pad that has no Done key.

The one thing to get right first: state is a string

Nearly every broken number input in React Native has the same shape: the component stores Number(text) in state and passes that number back as value. It works until the user types something that is not yet a number. “3.” becomes 3 and the dot disappears. “-” becomes NaN and the field shows “NaN”. Clearing the field becomes 0 and the user cannot delete the zero.

The fix is structural, not a patch. The input owns a string. Parsing happens somewhere else, when you need the value.

import { useState } from 'react';
import { TextInput } from 'react-native';

export function QuantityInput({ onChange }: { onChange: (n: number | null) => void }) {
  // The input owns a STRING. Always.
  const [text, setText] = useState('');

  const handleChange = (raw: string) => {
    // Enforce digits here. keyboardType does not block paste or hardware keys.
    const digits = raw.replace(/[^0-9]/g, '');
    setText(digits);
    onChange(digits === '' ? null : Number(digits));
  };

  return (
    <TextInput
      value={text}
      onChangeText={handleChange}
      keyboardType="number-pad"
      inputMode="numeric"
      maxLength={6}
      placeholder="0"
      accessibilityLabel="Quantity"
    />
  );
}

Two props do the keyboard work. keyboardType picks the native keyboard on iOS and Android. inputMode is the newer, web-aligned prop that does the same job and also drives the virtual keyboard on React Native Web. Set both; they do not conflict.

Which keyboardType, on which platform

The values are the same on both platforms. What they show is not, and the differences are exactly where bugs hide: iOS number pads have no minus key and no return key.

keyboardTypeiOSAndroidUse it for
number-padDigits only. No decimal, no minus, no return keyPhone-style digits. Return key presentQuantities, PINs, years, zip codes
decimal-padDigits plus the locale decimal separator. No minus, no return keyDigits plus decimal separatorPrices, weights, measurements
numericDigits with a punctuation row (. , - and more). Return key presentSame as number-padValues that may be negative on iOS
numbers-and-punctuationFull numeric and symbol layout. Return key presentFalls back to the default keyboardCoordinates, ranges like 10-20, iOS-only apps
phone-padDigits plus * and #Phone dialer layoutPhone numbers only

Default rule: number-pad for whole numbers, decimal-pad for anything with a fraction. Reach for numeric only when iOS users need a minus sign, and even then consider a separate +/- toggle, because a punctuation row next to digits invites typos.

Decimals: the comma problem

decimal-pad shows the decimal separator for the device locale. A German or Brazilian user gets a comma. If your parser only accepts a dot, Number("12,50") is NaN and the field looks broken in exactly the markets you did not test in. Normalise before parsing, and allow at most one separator:

export function sanitizeDecimal(raw: string, maxDecimals = 2) {
  // Accept either separator while typing, keep what the user typed on screen.
  let s = raw.replace(/[^0-9.,]/g, '');
  // Keep only the first separator.
  const firstSep = s.search(/[.,]/);
  if (firstSep !== -1) {
    const head = s.slice(0, firstSep + 1);
    const tail = s.slice(firstSep + 1).replace(/[.,]/g, '').slice(0, maxDecimals);
    s = head + tail;
  }
  return s;
}

export function parseDecimal(text: string): number | null {
  if (text === '' || text === '.' || text === ',') return null;
  const n = Number(text.replace(',', '.'));
  return Number.isFinite(n) ? n : null;
}

Keep what the user typed in the field, normalise only in parseDecimal. Silently swapping their comma for a dot mid-keystroke reads as a glitch, and some Android keyboards will then refuse to insert a second comma.

Currency input that formats as you type

The classic request: the user types 1250 and sees $12.50, then 12500 and sees $125.00. The classic failure: the cursor jumps to the wrong place after every keystroke, or the formatted string gets parsed back and loses a digit. Both come from treating the formatted display as the source of truth.

Store cents as a digit string. Format on render. Never parse the formatted string.

import { useMemo, useState } from 'react';
import { TextInput } from 'react-native';

const formatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });

export function CurrencyInput({ onChange }: { onChange: (cents: number) => void }) {
  // Source of truth: digits only, representing cents. "1250" => $12.50
  const [cents, setCents] = useState('');

  const display = useMemo(() => {
    if (cents === '') return '';
    return formatter.format(Number(cents) / 100);
  }, [cents]);

  const handleChange = (raw: string) => {
    // Whatever the user typed INTO the formatted string, we only care about digits.
    const digits = raw.replace(/[^0-9]/g, '').replace(/^0+(?=\d)/, '').slice(0, 12);
    setCents(digits);
    onChange(digits === '' ? 0 : Number(digits));
  };

  return (
    <TextInput
      value={display}
      onChangeText={handleChange}
      keyboardType="number-pad"
      inputMode="numeric"
      placeholder={formatter.format(0)}
      // Prevents iOS from offering to autofill a credit card number here.
      textContentType="none"
      accessibilityLabel="Amount in US dollars"
    />
  );
}

This works because money grows from the right. Every keystroke appends a digit, the formatted value gets longer on the right, and the cursor is always at the end. Backspace removes the last digit, the string shrinks, same story. The one real limitation: the user cannot place the cursor in the middle and edit a specific digit. For most payment and amount fields that is the behaviour people expect from an ATM-style input anyway.

If you genuinely need in-place editing of a formatted number (phone numbers, IBANs, card numbers), that is a mask problem, not a number-input problem. react-native-mask-input handles cursor tracking inside a mask, and react-native-currency-input wraps the same idea specifically for money with locale separators. Both are pure JavaScript and run in Expo Go.

The iOS number pad has no Done key

This one generates support tickets. number-pad and decimal-pad on iOS have no return key at all, so returnKeyType="done" and onSubmitEditing never fire. If the field is the last thing on the screen, the keyboard covers your button and the user is stuck.

Two fixes. The cheap one is a tap-outside dismiss:

import { Keyboard, Pressable } from 'react-native';

<Pressable style={{ flex: 1 }} onPress={Keyboard.dismiss} accessible={false}>
  {/* screen content */}
</Pressable>

The proper one is a toolbar above the keyboard. InputAccessoryView is the iOS-only core component for exactly this:

import { Button, InputAccessoryView, Keyboard, Platform, TextInput, View } from 'react-native';

const ACCESSORY_ID = 'amount-done';

<TextInput
  keyboardType="decimal-pad"
  inputAccessoryViewID={Platform.OS === 'ios' ? ACCESSORY_ID : undefined}
  /* ...value, onChangeText */
/>

{Platform.OS === 'ios' && (
  <InputAccessoryView nativeID={ACCESSORY_ID}>
    <View style={{ alignItems: 'flex-end', backgroundColor: '#f2f2f7', padding: 6 }}>
      <Button title="Done" onPress={Keyboard.dismiss} />
    </View>
  </InputAccessoryView>
)}

Android keeps its return key on every numeric keyboard, so the guard is harmless there. If you already use react-native-keyboard-controller, its KeyboardToolbar gives you the Done button plus next and previous field arrows on both platforms with no per-field wiring.

Negative numbers and a stepper

Since the iOS number pads have no minus key, the honest options are a punctuation keyboard (ugly) or a sign toggle next to the field (clear). For small ranges, a stepper with plus and minus buttons beats a keyboard entirely, and it is the pattern that gets the accessibility story right for free:

import { Pressable, Text, TextInput, View } from 'react-native';

type Props = { value: number; min?: number; max?: number; step?: number; onChange: (n: number) => void };

export function Stepper({ value, min = -Infinity, max = Infinity, step = 1, onChange }: Props) {
  const clamp = (n: number) => Math.min(max, Math.max(min, n));

  return (
    <View
      style={{ flexDirection: 'row', alignItems: 'center', gap: 8 }}
      accessibilityRole="adjustable"
      accessibilityLabel="Quantity"
      accessibilityValue={{ now: value, min: Number.isFinite(min) ? min : undefined, max: Number.isFinite(max) ? max : undefined }}
      accessibilityActions={[{ name: 'increment' }, { name: 'decrement' }]}
      onAccessibilityAction={(e) => {
        if (e.nativeEvent.actionName === 'increment') onChange(clamp(value + step));
        if (e.nativeEvent.actionName === 'decrement') onChange(clamp(value - step));
      }}
    >
      <Pressable onPress={() => onChange(clamp(value - step))} disabled={value <= min} hitSlop={8}>
        <Text style={{ fontSize: 22, width: 32, textAlign: 'center' }}>-</Text>
      </Pressable>
      <TextInput
        value={String(value)}
        onChangeText={(t) => {
          const n = Number(t.replace(/[^0-9-]/g, ''));
          if (Number.isFinite(n)) onChange(clamp(n));
        }}
        keyboardType="numeric"
        inputMode="numeric"
        style={{ minWidth: 48, textAlign: 'center' }}
      />
      <Pressable onPress={() => onChange(clamp(value + step))} disabled={value >= max} hitSlop={8}>
        <Text style={{ fontSize: 22, width: 32, textAlign: 'center' }}>+</Text>
      </Pressable>
    </View>
  );
}

accessibilityRole="adjustable" is the part most steppers skip. It lets VoiceOver and TalkBack users swipe up and down to change the value instead of hunting for two tiny buttons, which is the same gesture they use on every native slider and stepper.

Four small things that bite later

  • maxLength counts characters, not digits. On a formatted currency field, the dollar sign and separators eat into it. Enforce length on the raw digits in onChangeText, not with the prop.
  • Leading zeros.“007” parses to 7, which is fine for a quantity and wrong for a PIN or a zip code. Decide per field whether the string or the number is the real value.
  • Float precision. 0.1 + 0.2 is still 0.30000000000000004 in JavaScript. Store money as integer cents, as the currency example does, and only divide for display.
  • Autofill on iOS.A bare numeric field near words like “card” or “code” can trigger the credit card or one-time-code autofill bar. Set textContentType="none" on fields that are not those things, and textContentType="oneTimeCode" on the field that is. The OTP input guide covers that case end to end.

Skip the wiring

A string-backed input, locale-safe decimal parsing, a currency field that keeps its cursor, a Done bar on iOS. None of it is hard, all of it is the kind of thing you rewrite in every app and get slightly wrong once. ShipNative generates a real Expo app from a description with these form patterns already in place, previews it on your phone, and hands you the full React Native project to keep editing. Describe the form you need and change the number field like any other file you wrote.

Frequently Asked Questions

How do I make a number-only input in React Native?

Use a TextInput with keyboardType="numeric" (or "number-pad" for digits only) and inputMode="numeric", then strip anything that is not a digit in onChangeText. The keyboard type only changes which keyboard appears; it does not block pasted letters or hardware keyboards, so the filter in onChangeText is what actually enforces numbers. Keep the state as a string and parse it with Number() only when you need the value.

Why does my React Native number input turn into NaN or an empty field?

Because TextInput always gives you a string, and most people store Number(text) in state and feed it back as value. The moment the user types a partial value like "3." or "-" or clears the field, Number() returns NaN or 0, the input re-renders with that, and the user cannot finish typing. Store the raw string the user typed, validate it separately, and convert to a number at submit time.

What is the difference between keyboardType numeric, number-pad and decimal-pad?

On iOS, number-pad shows digits only, decimal-pad adds a decimal separator, and numeric shows digits with a small punctuation row. None of the three has a minus key on iOS. On Android, numeric and number-pad both open the phone-style number keyboard, and decimal-pad enables the decimal separator. If users need negative numbers, use numbers-and-punctuation on iOS or add your own minus toggle.

How do I add a Done button to the iOS number pad?

The iOS number-pad and decimal-pad keyboards have no return key, so returnKeyType does nothing. Either dismiss the keyboard yourself by wrapping the screen in a Pressable that calls Keyboard.dismiss on tap outside, or use InputAccessoryView to attach a toolbar with a Done button above the keyboard. react-native-keyboard-controller also ships a KeyboardToolbar that does this on both platforms.

How do I format a currency input in React Native without the cursor jumping?

Store the raw digits (cents) in state, format them to a display string with Intl.NumberFormat on every render, and never let the formatted string round-trip back through Number(). Because the input is fully controlled and the formatted value grows from the right, the cursor stays at the end, which is where users expect it for money. For free-form cursor placement inside a formatted number you need a mask library such as react-native-mask-input or react-native-currency-input.

→

React Native TextInput: Props, Keyboards, and Fixes

The full TextInput reference this guide builds on.

Read guide →
→

React Native Form Validation

Validating the parsed number with zod and react-hook-form.

Read guide →

Ship a real React Native app today

Describe, preview, and export Expo code, free to start.

Build with ShipNative
ShipNative

Build mobile apps with AI. Describe it, try it on your phone, own the code.

Features

Text to App AIApp Generator from ScreenshotPRD to Mobile App

Tools

All free toolsApp Cost CalculatorApp Name GeneratorApp Store Keyword ToolReact Native Components

Blog

All blog postsHow to Build an App Without CodingBest AI Tools for Real Mobile AppsExpo EAS App Store ChecklistLovable, Cursor & v0 for MobileBest AI App Builders in 2026React Native AI App Builder Guide
© 2026 ShipNative
PrivacyTermsAboutFAQPricingDiscordShipNative - Featured on Startup FameFeatured on Twelve ToolsFeatured on TinyShelfShipNative - Launched on Fazier