Text Input Numeric Keyboard on Mobile View?

Hello,

Is it possible to make the Text Input component display the numeric keypad layout in mobile view? The Number Input component works for this, but I’m experiencing an issue with its Change event, so I can’t use it until that’s fixed.
Number Input Component not reacting to "on change" instantly (only after clicking outside) - App Building - Retool Forum

Here’s a screenshot of the Text Input component showing a QWERTY keyboard:

And here’s a screenshot of the Number Input component displaying the numeric keypad:

Is there a way to force the Text Input component to use the numeric keypad in mobile view?

The purpose of this is to implement OTP fields that allow a user to automatically jump to the next box every time they type a number. Additionally, if they have a 6-digit code, they can paste it into one of the boxes, and it will auto-fill. The only problem is that on iOS, the keyboard defaults to QWERTY mode, making it difficult to input the One-Time Password.
{3EC3C0D7-5114-4B7F-A859-60293AC564AC}

Thank you!

Unfortunately we don't have any update on the numberInput request that would allow events to trigger on component change (without using stepper).

Can I ask if this for an app that you want to be using on mobile and on desktop? If it's for a mobile only use and you build the app as a mobile app then the numberInput component can be triggered onChange without using the stepper. The limitation you are describing is limited to Desktop apps (that can run on mobile).

Since the numberInput.value isn't reflected in the state until the user clicks outside the field, as you found, I can't think of a workaround to trigger the event. As you have also found we are stuck there.
image

I was trying to see what other ways you have around the blocker you are facing - unfortunately there's no way to force a number display on the textInput component input.

The editableNumber component has the same limitation here so that wouldn't work as a solution.

You could potentially use multiple day (date) components here :face_with_peeking_eye: as in this example
image

Or you could build a custom component for this.... but that would be a significant bit of work.

1 Like

Hi Alice,

We'll be using it on both mobile and desktop.

I also tried creating a custom OTP field component and added a change event handler using Retool.useEventCallback. However, I discovered that the Control Component has very limited options, such as the lack of a Focus option, and clearing the value.

To simplify things, I decided to redesign our OTP field to use a single number input like this:
image

Thanks.

1 Like

Hi @Alice, I found another issue with the Number Input Component: it truncates leading zeros. For example, 01 becomes 1. Unfortunately, I couldn't find a workaround because we need to use the Number Input Component to display a numeric keyboard on mobile devices.

Here’s what happens when you paste a code that starts with 0:
image

After clicking outside the number input component, 099174 becomes 99174:
image

Unfortunately, OTP code can start with zero values.

If there's no other way, then I'll try to create a custom component with

<input type="text" inputmode="numeric">

Thank you!

Sharing my custom component temporary solution:

It is not the most optimized code, but it works for now.

TextNumericInputComponent.tsx

import { Retool } from "@tryretool/custom-component-support"
import { FC } from "react"
import './index.css'

export const TextNumericInputComponent: FC = () => {

  Retool.useComponentSettings({
    defaultWidth: 12,
    defaultHeight: 8,
  });

  const [label] = Retool.useStateString({
    name: "label",
    initialValue: "Verification Code",
    label: "Label"
  });

  const [placeholder] = Retool.useStateString({
    name: "placeholder",
    initialValue: "XXXXXX",
    label: "Placeholder"
  });

  const [maxLength] = Retool.useStateNumber({
    name: "maxLength",
    initialValue: 6,
    label: "Max Length"
  });

  const [isRequired] = Retool.useStateBoolean({
    name: "isRequired",
    initialValue: true,
    label: "Required",
    inspector: "checkbox",
  });

  const [_otpValue, setOtpValue] = Retool.useStateString({
    name: "otpValue",
    inspector: "hidden"
  });

  return (
    <div>
      <label htmlFor="textNumericInputComponent" className="labelClass">
        {label} {isRequired && <span style={{ color: "red" }}>*</span>}
      </label>
      <input type="text"
        placeholder={placeholder}
        id="textNumericInputComponent"
        className="textNumericInputComponent"
        pattern="[0-9]*"
        maxLength={maxLength}
        inputMode="numeric"
        required={isRequired}
        onChange={e => setOtpValue(e.target.value)}
        aria-label={label}
      />
    </div>
  )
}

index.css:

/* Import Inter font */
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400&display=swap');

.textNumericInputComponent {
  width: 100%;
  height: 30px;
  font-size: 12px;
  /* font-family: Arial, sans-serif; */
  font-family: 'Inter', sans-serif;
  border: 1px solid #ccc;
  border-radius: 4px; /* Slight rounded edges */
  padding: 12px; /* Inner spacing */
  outline: none; /* Remove default outline */
  box-sizing: border-box; /* Padding included in width */
  transition: border-color 0.3s ease, box-shadow 0.3s ease; /* Smooth transitions */
  letter-spacing: 0.5px;
}

.textNumericInputComponent:focus {
  border-color: #8104DB; /* Purple border color on focus */
  box-shadow: 0 0 5px rgba(129, 4, 219, 0.5); /* Slight purple glow */
}

.labelClass {
  font-size: 12px;
  font-family: 'Inter', sans-serif; /* Set label font to Inter */
  font-weight: 600;
  margin-bottom: 4px;
  display: block;
}

Inspector View:

Result:
Top one is a Number Input Component, and the bottom one is the custom component.

{50C85BB0-D802-4B3B-B93C-A0D172D00B8F}