Skip to main content
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Cage Sarin
Ben Nadel at CFCamp 2023 (Freising, Germany) with: Cage Sarin

Dynamically Swapping Input / Keyboard InputMode On iOS

By on

In Dig Deep Fitness, my ColdFusion fitness tracker, exercise weights and reps (repetitions) are stored as text. This provides greater real-world flexibility and allows users to enter text values like "BW" (Body Weight) and "Red Band" and "Large Sand Bag". That said, the majority of cases will almost certainly be the entering of standard, numeric weights and reps. This poses a problem. I'd like the default keyboard for my exercise sets to be the Numeric keyboard, as this has large, chonky buttons that make data entry easier. But, I still want the user to be able to switch to the Text keyboard for edge-cases. Unfortunately, iOS doesn't provide a way to switch from the numeric keyboard back to the text keyboard. As such, we have come up with hacky ways to build this functionality into the user interface (UI).

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

My iOS device will change the keyboard that it opens based on the inputmode of the focused Input control. But, if we dynamically change the inputmode while the keyboard is already open, iOS ignores the change, leaving the previously-rendered keyboard in view.

Over on StackOverflow, Nate Adams shared an approach (that apparently took inspiration from Nike) to temporarily focus another input while changing the inputmode; then, to re-focus the original input with a new inputmode wherein iOS will render the new keyboard.

This gives us a foothold into a technical workflow; but, we still have an issue in that the iOS Numeric keyboard doesn't actually have a button to get back to the Text keyboard. As such, we have to provide a trigger in the UI (User Interface) of the page.

To experiment with this approach, I wanted to create an input control that has a "built in" button for toggling the inputmode. The Input and the Button are displayed side-by-side using display: flex; and, attempt to create a sense of unification. Clicking on the button will toggle between decimal and text for the inputmode property.

CAUTION: This is not a production-ready solution - this is just a proof-of-concept. It has several issues, not the least of which is the fact that the buttons will show on Desktop devices where they serve no purpose. A full, production-ready solution would certainly include some sort of progressive enhancement with something like CSS media-queries.

<!doctype html>
<html lang="en">
<head>
	<meta charset="utf-8" />
	<meta name="viewport" content="width=device-width, initial-scale=1" />
	<style type="text/css">

		html {
			box-sizing: border-box ;
		}
		html *,
		html *:before,
		html *:after {
			box-sizing: inherit ;
		}

		.numberish {
			border-radius: 1px ;
			display: flex ;
			font-size: 18px ;
			position: relative ;
			width: 100% ;
		}
		.numberish__input {
			border: 1px solid #cccccc ;
			border-radius: 3px 0px 0px 3px ;
			flex: 1 1 auto ;
			font-size: inherit ;
			font-weight: inherit ;
			margin: 0px 0px 0px 0px ;
			padding: 10px 10px 10px 13px ;
		}
		.numberish__switch {
			border: 1px solid #cccccc ;
			border-radius: 0px 3px 3px 0px ;
			border-left-width: 0px ;
			color: #333333 ;
			flex: 0 0 auto ;
			font-size: 20px ;
			letter-spacing: 1px ;
			margin: 0px 0px 0px 0px ;
			padding: 0px 15px 0px 15px ;
		}
		.numberish__temp {
			left: 0px ;
			opacity: 0 ;
			pointer-events: none ; /* No clicking allowed! */
			position: absolute ;
			top: 0px ;
		}

		/* Set button text based on state of control. */
		.numberish--numeric .numberish__switch:before {
			content: "Aa" ;
		}
		.numberish--alpha .numberish__switch:before {
			content: "123" ;
		}

		/*
			Prevent individual elements from being outlined - keep outline around the
			entire control when either sub-element is focused.
		*/
		.numberish:focus-within {
			outline: 2px solid blue ;
			outline-offset: 2px ;
		}
		.numberish__input:focus,
		.numberish__switch:focus {
			outline: none ;
		}

	</style>
</head>
<body>

	<h1>
		Swapping Keyboard InputMode Dynamically on iOS
	</h1>

	<div class="numberish">
		<input
			type="text"
			class="numberish__input"
		/>
		<button
			aria-hidden="true"
			type="button"
			tabindex="-1"
			class="numberish__switch">
			<!-- Text is set via CSS content. -->
		</button>
		<!--
			In order to trigger the keyboard change on iOS, we have to focus ANOTHER input
			briefly before then returning the focus to the intended input with the updated
			inputmode. This temp input will not be visible or tabbable by the user, but
			will be focused programmatically when the user wants to switch keyboards.
		-->
		<input
			aria-hidden="true"
			type="text"
			tabindex="-1"
			class="numberish__temp"
		/>
	</div>

	<script type="text/javascript">

		// Centralize the input-mode settings.
		var numericInputmode = "decimal";
		var numericPattern = "[0-9.]*";
		var alphaInputmode = "text";
		var alphaPattern = ".*";

		// Cache DOM element references.
		var numberish = document.querySelector( ".numberish" );
		var input = numberish.querySelector( ".numberish__input" );
		var button = numberish.querySelector( ".numberish__switch" );
		var temp = numberish.querySelector( ".numberish__temp" );

		// Initialize the input setup (default to numeric keyboard).
		numberish.classList.add( "numberish--numeric" );
		input.inputMode = numericInputmode;
		input.pattern = numericPattern;
		button.addEventListener( "click", toggleInputType );

		/**
		* I switch the control from one type of keyboard to the other.
		*/
		function toggleInputType() {

			// Switching to alpha keyboard.
			if ( numberish.classList.contains( "numberish--numeric" ) ) {

				numberish.classList.remove( "numberish--numeric" );
				numberish.classList.add( "numberish--alpha" );
				input.inputMode = temp.inputMode = alphaInputmode;
				input.pattern = temp.pattern = alphaPattern;

			// Switching to numeric keyboard.
			} else {

				numberish.classList.remove( "numberish--alpha" );
				numberish.classList.add( "numberish--numeric" );
				input.inputMode = temp.inputMode = numericInputmode;
				input.pattern = temp.pattern = numericPattern;

			}

			// When the input is focused, the keyboard won't update (based on the
			// inputmode). As such, we have to briefly focus the temp input and then
			// re-focus the main input.
			// --
			// Concept From StackOverflow: https://stackoverflow.com/a/55425845
			temp.focus();

			window.requestAnimationFrame(
				() => {

					input.focus();
					// Move caret to end of input value.
					input.setSelectionRange( input.value.length, input.value.length );

				}
			);

		}

	</script>

</body>
</html>

As you can see, when the user clicks the button in the UI, we:

  1. Change the inputmode and pattern settings of the Input.

  2. Focus the hidden temp input briefly.

  3. Re-Focus the main visible input.

And, when we do so, we get the following experience on Chrome (and Safari) for iOS:

Toggling back and forth between two inputmodes on the same input on Chrome for iOS.

As you can see, when I hit the Aa/123 button in the control, it (seems to from a User Experience perspective) leave the input focused and immediately changes from one keyboard to another.

I don't have an Android device, so I wasn't able to see if this approach works on Android. And, actually, I think I remember seeing in a screenshot that Android allows the user to switch between Numeric and Text keyboards, so it probably isn't even necessary on Android - yet another reason why this isn't a production-ready approach.

If nothing else, this gives me something to think about in terms of possible user experiences.

Want to use code from this post? Check out the license.

Reader Comments

15,688 Comments

One thing to be careful of with inputmode; or, more specifically with pattern is that it can trigger the browser's native form validation. This may or may not be what you want.

In my case, I ran into a situation where all my inputs were truly "text", but I was using the inputmode/pattern changes to swap the keyboard as a convenience, not as a validation. However, when a non-numeric value was in my numeric keyboard field, the form was preventing submission.

To fix this, I had to add novalidate to the form element:

<form method="post" novalidate>

Then, it doesn't matter what is in the field.

Post A Comment — I'd Love To Hear From You!

Post a Comment

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel