Start Debugging

Fix: .NET MAUI Entry with Keyboard.Numeric shows a small numpad, then the full keyboard, on iPadOS 26

On iPadOS 26, MAUI maps Keyboard.Numeric to UIKeyboardType.DecimalPad, which now opens as a floating mini numpad. Append a mapper that switches iPad to NumbersAndPunctuation and filter input yourself.

If an Entry with Keyboard="Numeric" in .NET MAUI 10 opens a small floating number pad the first time you tap it on an iPad, and then the full-width keyboard on the numbers page the next time (sometimes with no decimal point, no minus sign, or keys that type the wrong digit), MAUI is not switching keyboards on you. iPadOS 26 is. MAUI maps Keyboard.Numeric to UIKeyboardType.DecimalPad, and on iPadOS 26 that keyboard type is presented as a compact floating keypad when no keyboard is docked yet. The fix that works today is to append to EntryHandler.Mapper so numeric entries on iPad get UIKeyboardType.NumbersAndPunctuation instead, then validate the text yourself, because that keyboard can also type letters. iPhone is not affected and keeps the regular decimal pad.

The error in context

There is no exception. The symptom is a keyboard that changes shape between focus events. On an iPad running iPadOS 26.0 or 26.1 with a plain MAUI numeric Entry:

// .NET 10, Microsoft.Maui.Controls 10.0.x, iPad (10th gen), iPadOS 26.1
1st tap on Entry   -> small floating number pad (digits + decimal key), no Done bar
tap outside        -> keypad dismisses
2nd tap on Entry   -> full-width keyboard, numbers-and-symbols page
type "12.5"        -> field shows unexpected characters on some devices
switch to ABC page and back to 123 -> digits register correctly again

People searching for this usually describe one slice of it: “Keyboard.Numeric not working on iOS”, “no decimal point on iPad numeric keyboard”, “numeric keypad floating on iPad”, “MAUI Done button missing on iPad”, or “numbers type random characters on iPad”. The MAUI tracking issue is dotnet/maui#32288 (iPad 8th gen, iOS 26.0.1, MAUI 10.0.0-rc.2, flagged as a regression and still open in the backlog). The same behavior shows up in Flutter as flutter/flutter#178096 and in native UIKit apps, which is the tell that this is not a MAUI bug.

Why iPadOS 26 swaps the keyboard under a MAUI numeric Entry

MAUI’s iOS keyboard mapping is short. In KeyboardExtensions.cs on main, ApplyKeyboard does this for the numeric case:

// .NET MAUI main (10.0.x and 11 previews), src/Core/src/Platform/iOS/KeyboardExtensions.cs
else if (keyboard == Keyboard.Numeric)
    textInput.SetKeyboardType(UIKeyboardType.DecimalPad);
else if (keyboard == Keyboard.Telephone)
    textInput.SetKeyboardType(UIKeyboardType.PhonePad);

Before iPadOS 26, DecimalPad on an iPad simply opened the regular full keyboard on its numbers page, because iPad never had a dedicated number pad. iPadOS 26 changed that: decimalPad and numberPad now present a compact, floating number-only panel. Developers hit three separate problems with it:

  1. The floating pad appears only when no keyboard is docked. If the user was typing in a text field and moves focus to the numeric field, the docked keyboard stays up and flips to its numbers page. If the numeric field is the first responder, you get the floating pad. The Flutter issue documents exactly this: it works when focus moves from a text field and misbehaves when the numeric field is focused first. That is why the keyboard looks like it “switches” depending on what the user tapped before.
  2. The floating pad hides inputAccessoryView. MAUI attaches its own MauiDoneAccessoryView to every iOS Entry in EntryHandler.CreatePlatformView(). The Apple Developer Forums thread 801458 reports that the floating keypad does not show the accessory toolbar, so your Done bar, and any custom Next/Previous toolbar, disappears.
  3. Dismiss-and-refocus produces the full keyboard with bad input. Thread 808114 (FB21144039) reproduces it in Apple’s own Contacts app on iPadOS 26.0 through 26.1: tap a numeric field, get the small pad, dismiss it, tap again, get the full keyboard on the numbers page, and the keys register the wrong characters until you switch to the letters page and back.

None of this is under MAUI’s control, and there is no UIKit API to opt out of the floating keypad. What you can control is which UIKeyboardType the text field asks for.

Minimal repro

<!-- .NET 10, Microsoft.Maui.Controls 10.0.x, run on an iPad with iPadOS 26.0 or 26.1 -->
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="KeyboardRepro.MainPage">
    <VerticalStackLayout Padding="24" Spacing="16">
        <Entry Placeholder="Amount" Keyboard="Numeric" />
        <Entry Placeholder="Notes" />
    </VerticalStackLayout>
</ContentPage>

Launch the app, tap “Amount” first: floating numpad. Tap outside, tap “Amount” again: full keyboard. Now tap “Notes” first, then “Amount”: docked keyboard on the numbers page, no floating pad. Same code, three different keyboards, decided entirely by focus history.

Fix 1: map numeric entries to NumbersAndPunctuation on iPad

This is the workaround the Apple forum converged on, translated to a MAUI handler mapping. NumbersAndPunctuation always docks, keeps the accessory view visible, has a decimal separator and a minus sign, and has behaved the same way on iPad for many iOS releases.

Add this to MauiProgram.cs:

// .NET 10, Microsoft.Maui.Controls 10.0.x, iOS/iPadOS 26
using Microsoft.Maui.Handlers;
#if IOS
using UIKit;
#endif

public static class MauiProgram
{
    public static MauiApp CreateMauiApp()
    {
        var builder = MauiApp.CreateBuilder();
        builder.UseMauiApp<App>();

#if IOS
        // Runs after MAUI's own "Keyboard" mapping, so it overrides DecimalPad.
        EntryHandler.Mapper.AppendToMapping(nameof(IEntry.Keyboard), (handler, entry) =>
        {
            if (entry.Keyboard != Keyboard.Numeric)
                return;

            if (UIDevice.CurrentDevice.UserInterfaceIdiom != UIUserInterfaceIdiom.Pad)
                return;

            if (!OperatingSystem.IsIOSVersionAtLeast(26))
                return;

            handler.PlatformView.KeyboardType = UIKeyboardType.NumbersAndPunctuation;
            handler.PlatformView.ReloadInputViews();
        });
#endif

        return builder.Build();
    }
}

Three details matter here:

OperatingSystem.IsIOSVersionAtLeast(26) returns true on iPadOS because iPadOS reports itself as iOS. The version check keeps iPads still on iPadOS 18 on the old, correct DecimalPad path. I deliberately did not add an upper bound: Apple has not documented a change in behavior, so re-test on each new iPadOS release before you remove the mapping rather than guessing a version where it goes away.

Fix 2: validate the text, because NumbersAndPunctuation is not numeric-only

NumbersAndPunctuation is the numbers page of the full keyboard. The user can tap “ABC” and type letters, and they can type several decimal separators. DecimalPad never let that happen, so code that did decimal.Parse(entry.Text) without a guard will start throwing FormatException.

A small behavior that rejects anything that cannot become a number fixes it:

// .NET 10, Microsoft.Maui.Controls 10.0.x
using System.Globalization;

public sealed class DecimalInputBehavior : Behavior<Entry>
{
    protected override void OnAttachedTo(Entry entry)
    {
        entry.TextChanged += OnTextChanged;
        base.OnAttachedTo(entry);
    }

    protected override void OnDetachingFrom(Entry entry)
    {
        entry.TextChanged -= OnTextChanged;
        base.OnDetachingFrom(entry);
    }

    static void OnTextChanged(object? sender, TextChangedEventArgs e)
    {
        if (sender is not Entry entry || string.IsNullOrEmpty(e.NewTextValue))
            return;

        // Appending "0" lets partial input like "-", "12." or "," pass while typing.
        var candidate = e.NewTextValue + "0";
        var ok = decimal.TryParse(
            candidate,
            NumberStyles.AllowLeadingSign | NumberStyles.AllowDecimalPoint,
            CultureInfo.CurrentCulture,
            out _);

        if (!ok)
            entry.Text = e.OldTextValue;
    }
}
<!-- .NET 10, Microsoft.Maui.Controls 10.0.x -->
<Entry Placeholder="Amount" Keyboard="Numeric" ReturnType="Done">
    <Entry.Behaviors>
        <local:DecimalInputBehavior />
    </Entry.Behaviors>
</Entry>

Two reasons to do this in the shared layer instead of hooking UITextField.ShouldChangeCharacters on iOS: MAUI’s EntryHandler already owns that delegate to enforce MaxLength (and on iOS 26 it uses the newer multi-range variant), so replacing it silently breaks MaxLength. And the behavior protects Android and Windows too, where a hardware keyboard can type anything into a numeric field.

Parsing with CultureInfo.CurrentCulture matters. On a German iPad the numbers page shows a comma, and "12,5" must parse. If your backend expects invariant input, convert once when you read the value, not while the user types.

Fix 3: restore the Done key on iPad

With DecimalPad on iPhone, MAUI’s MauiDoneAccessoryView gives you a Done button, because the iPhone decimal pad has no return key. NumbersAndPunctuation has a return key, so set ReturnType="Done" (as in the XAML above) and handle Completed:

// .NET 10, Microsoft.Maui.Controls 10.0.x
AmountEntry.Completed += (_, _) =>
{
    AmountEntry.Unfocus();
    // Commit the value, move focus to the next field, etc.
};

On iPhone nothing changes: the mapping in Fix 1 never runs there, the decimal pad stays, and the accessory Done bar keeps working.

Opting in per Entry instead of globally

A global mapping changes every numeric Entry in the app, including ones inside third-party controls. If you only want it on some fields, subclass Entry and check the type in the mapping:

// .NET 10, Microsoft.Maui.Controls 10.0.x
public class AmountEntry : Entry
{
    public AmountEntry() => Keyboard = Keyboard.Numeric;
}

#if IOS
EntryHandler.Mapper.AppendToMapping(nameof(IEntry.Keyboard), (handler, entry) =>
{
    if (entry is AmountEntry &&
        UIDevice.CurrentDevice.UserInterfaceIdiom == UIUserInterfaceIdiom.Pad &&
        OperatingSystem.IsIOSVersionAtLeast(26))
    {
        handler.PlatformView.KeyboardType = UIKeyboardType.NumbersAndPunctuation;
        handler.PlatformView.ReloadInputViews();
    }
});
#endif

The mapper is still global (it is a static on EntryHandler), but the type check scopes the effect. This is the same pattern MAUI uses for any platform tweak it does not expose as a property; the handler customization docs on Microsoft Learn cover the AppendToMapping ordering in detail.

Gotchas and lookalikes

Sources

Comments

Sign in with GitHub to comment. Reactions and replies thread back to the comments repo.

< Back