Skip to main content
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Matt Vickers and Jonathan Dowdle and Joel Hill
Ben Nadel at cf.Objective() 2014 (Bloomington, MN) with: Matt Vickers ( @envex ) Jonathan Dowdle ( @jdowdle ) Joel Hill ( @Jiggidyuo )

Capturing Keyboard Event Modifiers Across Operating Systems In JavaScript

By 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!

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

Reader Comments

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