DEV Community

Cover image for Better Numeric Inputs in Angular (Signal Forms + Angular 22)
Brian Treese
Brian Treese

Posted on • Originally published at briantree.se

Better Numeric Inputs in Angular (Signal Forms + Angular 22)

Signal Forms just fixed a subtle, but important, issue you’ve likely shipped without realizing. If you’re using <input type="number">, it's likely that you're introducing UX issues that only show up during real interaction. In this example, I'll show you a better approach that will be available in Angular v22.

Why Number Inputs Break UX in Angular Forms

Let's start with a typical setup.

We have a typed form model where age is a number and can be null:

interface SignupFormData {
  username: string;
  email: string;
  age: number | null;
}
Enter fullscreen mode Exit fullscreen mode

Then we have a signal-backed model to store the form data where the age field is initialized to null:

protected model = signal<SignupFormData>({
  username: '',
  email: '',
  age: null,
});
Enter fullscreen mode Exit fullscreen mode

After this, we have the form configuration created with the form() function from the Signal Forms API and our model signal:

protected signupForm = form(this.model, s => {
  required(s.username, { message: 'Username is required' });
  required(s.email, { message: 'Email is required' });
  required(s.age, { message: 'Age is required' });
});
Enter fullscreen mode Exit fullscreen mode

This form is already setup with basic required() validators for the username, email, and age fields.

So this is what we're starting with.

Now let's finish adding the logic for the age field.

First, we want to prevent folks from joining if they're under 18, so let's add a min() validator.

min(s.age, 18, { message: 'You must be at least 18' });
Enter fullscreen mode Exit fullscreen mode

Then, we want to ensure the age entered is valid.

If it's greater than 120, it's probably not valid, so let's add a max() validator:

max(s.age, 120, { message: 'Please enter a valid age' });
Enter fullscreen mode Exit fullscreen mode

That’s all we need here.

Now let’s switch over to the template and add the field itself.

Since age will always be a number, we should use a number input, right?

Let's try it!

We'll add a number type input and bind it to the age field using the formField directive.

<input 
  type="number" 
  [formField]="signupForm.age" />
Enter fullscreen mode Exit fullscreen mode

Now, this probably looks correct, but it really isn’t.

This is one of those cases where the default looks right but causes subtle issues in real use.

For one, the browser will automatically add a spinner control to the input:

The number input with a spinner control

These are rarely useful except in cases where you actually have an incremental number.

Which maybe you could argue we have here, but who wants to enter their age this way?

Also, if you use your mousewheel over the input, it will change the value too.

The number input with a mousewheel

In our case this isn't too bad but think of something like a postal code or credit card CVV number.

It just wouldn't make sense.

And this isn't just something I'm making up, MDN explicitly recommends avoiding number inputs in many cases.

So, let's switch over to the recommended approach.

Step 1: Switch to a Text Input

For this, all we need to do is replace this:

<input 
  type="number" 
  [formField]="signupForm.age" />
Enter fullscreen mode Exit fullscreen mode

With this:

<input 
  type="text" 
  inputmode="numeric" 
  [formField]="signupForm.age" />
Enter fullscreen mode Exit fullscreen mode

This removes browser-controlled behavior while still triggering the numeric keyboard on mobile thanks to the inputmode attribute.

At this point (pre-Angular 22), this breaks typing:

The number input with a typing issue

Step 2: Angular 22 Fix: Number ↔ Text Binding

Previously, with Signal Forms:

  • Text input → string
  • Model expects → number | null
  • Result → type mismatch

Angular 22 fixes this.

After upgrading, Signal Forms:

  • Accept text inputs for numeric fields
  • Convert values to number
  • Map empty input to null (not '')

This is the key improvement.

Step 3: Keep Validation in the Schema

If we want to strictly adhere to the MDN guidance we would add attributes like these:

<input 
  pattern="[0-9]*" 
  min="18" 
  max="120" />
Enter fullscreen mode Exit fullscreen mode

But these aren’t needed here.

With Signal Forms:

  • Validation belongs in the schema
  • Template stays declarative

So we'll keep the validation where we have it, and we'll remove these attributes.


If you're serious about leveling up your Angular skills, there's now an official certification path worth exploring.

Built with input from Google Developer Experts, it focuses on real-world Angular knowledge.

👉 Details here: https://bit.ly/4tfqleD



Step 4: Restrict Input via Keyboard Handling

According to MDN, browsers are inconsistent at enforcing numeric input, even with the correct inputmode.

So we need to enforce it ourselves.

To do this, we'll add a (keydown) event handler to the input.

We'll call it onAgeKeydown and it will take a KeyboardEvent parameter.

<input
  type="text"
  inputmode="numeric"
  [formField]="signupForm.age"
  (keydown)="onAgeKeydown($event)"
/>
Enter fullscreen mode Exit fullscreen mode

Then, we'll switch over to the component TypeScript and add this new method:

protected onAgeKeydown(event: KeyboardEvent) {
  const allowedKeys = [
    'Backspace',
    'Delete',
    'Tab',
    'Escape',
    'Enter',
    'ArrowLeft',
    'ArrowRight'
  ];

  if (allowedKeys.includes(event.key)) {
    return;
  }

  if (!/^\d$/.test(event.key)) {
    event.preventDefault();
  }
}
Enter fullscreen mode Exit fullscreen mode

This ensures:

  • Only digits are entered
  • Navigation keys still work

The Final Result

So now, we no longer have the spinner UI or scroll-wheel side effects:

The number input with the final result

We still can't add any non-numeric characters.

If we add invalid age values, we'll get the validation errors we added earlier:

The number input with validation errors

Then, when we clear the field, we'll get the correct null value:

The number input with the clear field

Previously, a text field would give you an empty string, but Angular now handles the conversion to null for us perfectly!

Clean, Typed Numeric Input in Angular 22

<input type="number"> looks correct but introduces avoidable UX issues.

Angular 22 removes the main blocker:

  • You can bind numeric models to text inputs cleanly
  • You retain strict typing and schema validation

For real applications, this is the better default (in many cases):

  • type="text"
  • inputmode="numeric"
  • Schema validation
  • Explicit keyboard handling

It’s a small change that eliminates a class of subtle UX bugs that often slip through reviews.

Taking This Further with Signal Forms

This example is just one piece of what Signal Forms are starting to simplify.

If you want to go deeper, I put together a full course that walks through building real-world forms step by step.

You can access it either directly or through YouTube membership, depending on what works best for you:

👉 Buy the course

👉 Get it with YouTube membership

Additional Resources

Top comments (0)