Skip to main content
Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.

Capturing Keyboard Event Modifiers Across Operating Systems In JavaScript

By Ben Nadel on

I have something rather embarrassing to admit. At some point, for reasons that I can't remember, when I switched over to using a MacBook about a decade ago, I somehow got it in my head that the event.metaKey represented a "generic modifier" that worked across operating systems. Meaning, I assumed that the event.metaKey was associated with different key combinations on different operating systems. To be clear, this is wrong. And, for years, I've built some key-combinations that only work on MacOS. Which is crazy, especially considering that no one ever complained about it! I suppose that's because most of those key-combinations are "progressive enhancements" to the user experience (UX). That said, I wanted to write this down so that I never forget that different keyboard event modifiers need to be captured on different operating systems.

Run this demo in my JavaScript Demos project on GitHub.

View this code in my JavaScript Demos project on GitHub.

CAUTION: At the outset of this demo, I want to be clear that detecting the current operating system is a bit of a dark-art. Every approach that I can find currently uses some form of window.navigator inspection. And, the properties being inspected are typically deprecated by web standards and are only provided by browsers for backwards compatibility. As such, we should take care to isolate this logic somewhere in our application in a way that is abstracted and reusable. This way, when / if it breaks, we only need to update the logic in a single point-of-failure.

That said, I grabbed the OS-detection logic from the tinykeys library, which uses the navigator.platform property. Thanks to Matt Vickers for this suggestion!

On the Mac - and related Mac platforms - the common keyboard modifier is the Meta key (aka, the Command key). On all other platforms, the common keyboard modifier is the Control key. So, to intercept the "Print Page" command on the Mac, we'd have to bind to the Command+P key-combination; and, to do the same on Windows, we'd have to bind to the Control+P key-combination.

To paper-over this divergence, a number of keyboard event-binding libraries will provide a pseudo key - Mod (Modifier) - that allows the developer to define key-combinations without having to use OS-specific modifiers. So, instead of Command+P for printing, the developer would use Mod+P and the event-binding library translates that to the OS-specific version.

To showcase this concept, this demo simply detects keydown events and determines whether or not those events have been "modified". It does this by building-up an OS-specific modifier object on boot-up; and then, uses that object to inspect different keys on the subsequent event objects.

<!doctype html>
<html lang="en">
<head>
	<meta charset="utf-8" />
	<title>
		Capturing Keyboard Event Modifiers Across Operating Systems In JavaScript
	</title>

	<link rel="stylesheet" type="text/css" href="./demo.css" />
</head>
<body>

	<h1>
		Capturing Keyboard Event Modifiers Across Operating Systems In JavaScript
	</h1>

	<p>
		<strong>OS Modifier:</strong>
		<code class="osModifier">
			<!-- To be populated via JavaScript. -->
		</code>
	</p>

	<dl class="event">
		<div class="key">
			<dt>Key</dt>
			<dd><!-- To be populated via JavaScript. --></dd>
		</div>
		<div class="modifier">
			<dt>Modified</dt>
			<dd><!-- To be populated via JavaScript. --></dd>
		</div>
	</dl>

	<script type="text/javascript">

		// The "modifier" key is different across the various operating systems (OS).
		// Let's calculate both the human-readable key (for UI hints) and the event
		// property that we're going to use when checking for a modified key-operation.
		var osModifier = buildOsModifier();

		// Gather our DOM nodes to populate.
		var osModifierNode = document.querySelector( ".osModifier" );
		var keyNode = document.querySelector( ".event .key dd" );
		var modifierNode = document.querySelector( ".event .modifier dd" );

		// Render which modifier key the current OS is using.
		osModifierNode.textContent = osModifier.long;

		window.addEventListener( "keydown", handleKeydown );

		// --------------------------------------------------------------------------- //
		// --------------------------------------------------------------------------- //

		// I handle the keydown event on the Window.
		function handleKeydown( event ) {

			// If this is the REFRESH PAGE event, allow the default behavior to happen -
			// I need this to work so that building this demo isn't a total pain.
			if ( isPageRefreshEvent( event ) ) {

				return;

			}

			// Since we want to capture and render key combinations that have system
			// modifiers, we need to prevent the default behavior (otherwise the demo
			// will do some funky stuff).
			event.preventDefault();

			// Render the key properties to the page.
			keyNode.textContent = event.key;
			// Check the OS-specific property for modification. On the MacOS, this will
			// be the "meta" key; and elsewhere, it will be the "control" key.
			modifierNode.textContent = event[ osModifier.modifier ]
				? "Yes"
				: "No"
			;

		}


		// I build the modifier object that will best fit the current operating system.
		function buildOsModifier() {

			if ( isMacish() ) {

				return({
					os: "Mac(ish)",
					long: "Command",    // For human-facing views.
					short: "Cmd",       // For human-facing views.
					modifier: "metaKey" // For event property inspection.
				});

			} else {

				return({
					os: "Non-Mac(ish)",
					long: "Control",    // For human-facing views.
					short: "Ctrl",      // For human-facing views.
					modifier: "ctrlKey" // For event property inspection.
				});

			}

		}


		// I determine if the current OS is Mac related (MacOS, iOS, etc).
		// --
		// CAUTION: The "navigator.platform" property - and other properties within the
		// navigator object - are deprecated. As such, we should take care to make sure
		// that OS-detection is isolated within a function or service that can be re-used
		// across our application. This way, when / if it breaks, we only need to update
		// the operating-system detection logic within a single point-of-failure.
		function isMacish() {

			// Pattern borrowed from TinyKeys library.
			// --
			// https://github.com/jamiebuilds/tinykeys/blob/e0d23b4f248af59ffbbe52411505c3d681c73045/src/tinykeys.ts#L50-L54
			var macOsPattern = /Mac|iPod|iPhone|iPad/;

			return( macOsPattern.test( window.navigator.platform ) );

		}


		// I determine if the given event is for a page-refresh operation.
		function isPageRefreshEvent( event ) {

			return( ( event.key === "r" ) && event[ osModifier.modifier ] );

		}

	</script>

</body>
</html>

As you can see, when this page runs, we create a osModifier object. This object contains an operating-system-specific configuration. It is the "abstraction" that we can use later when inspecting the keyboard event. And, in fact, when we do inspect the event, you can see that we are using the osModifier.modifier property:

event[ osModifier.modifier ]

Now, if we run this on the MacOS and try using both the Command (Meta) key and the Control key, you can see that only the Command key is considered a modifier:

Keyboard event modifier detection on MacOS.

And, if I boot-up my VirtualBox Windows image, you can see that when I use the Control key, that is the modifier:

Keyboard event modifier detection on Windows.

So, anyway, this was all just a note to myself so that I don't even forget about cross-operating-system keyboard event bindings again!



Reader Comments

What has two thumbs and hopes you leave a comment? This Guy! (Ben Nadel).

Post A Comment

You — Get Out Of My Dreams, Get Into My Blog
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.