Keyboard remapping on macOS with Karabiner-Elements for cross-platform work
Published on 20th November 2025 Estimated Reading Time: 4 minutesThis is something that I have been planing to share for a while; working across macOS, Linux and Windows poses a challenge to muscle memory when it comes to keyboard shortcuts. Since the macOS set up varies from the others, it was that which I set to harmonise with the others. Though the result is not full compatibility, it is close enough for my needs.
The need led me to install Karabiner-Elements and Karabiner-EventViewer. The latter has its uses for identifying which key is which on a keyboard, which happens to be essential when you are not using a Mac keyboard. While it is not needed all the time, the tool is a godsend when doing key mappings.
Karabiner-Elements is what holds the key mappings and needs to run all the time for them to be activated. Some are simple and others are complex; it helps the website is laden with examples of the latter. Maybe that is how an LLM can advise on how to set up things, too. Before we come to the ones that I use, here are the simple mappings that are active on my Mac Mini:
left_command → left_control
left_comtrol → left_command
This swaps the left-hand Command and Control keys while leaving their right-hand ones alone. It means that the original functionality is left for some cases when changing it for the keys that I use the most. However, I now find that I need to use the Command key in the Terminal instead of the Control counterpart that I used before the change, a counterintuitive situation that I overlook given how often the swap is needed in other places like remote Linux and Windows sessions.
grave_accent_and_tilde → non_us_backslash
non_us_backslash → non_us_pound
non_us_pound → grave_accent_and_tilde
It took a while to get this three-way switch figured out, and it is a bit fiddly too. All the effort was in the name of getting backslash and hash (pound in the US) keys the right way around for me, especially in those remote desktop sessions. What made the thing really tricky was the need to deal with Shift key behaviour, which necessitated the following script:
{
"description": "Map grave/tilde key to # and ~ (forced behaviour, detects Shift)",
"manipulators": [
{
"conditions": [
{
"name": "shift_held",
"type": "variable_if",
"value": 1
}
],
"from": {
"key_code": "grave_accent_and_tilde",
"modifiers": { "optional": ["any"] }
},
"to": [{ "shell_command": "osascript -e 'tell application \"System Events\" to keystroke \"~\"'" }],
"type": "basic"
},
{
"conditions": [
{
"name": "shift_held",
"type": "variable_unless",
"value": 1
}
],
"from": {
"key_code": "grave_accent_and_tilde",
"modifiers": { "optional": ["any"] }
},
"to": [
{
"key_code": "3",
"modifiers": ["option"]
}
],
"type": "basic"
},
{
"from": { "key_code": "left_shift" },
"to": [
{
"set_variable": {
"name": "shift_held",
"value": 1
}
},
{ "key_code": "left_shift" }
],
"to_after_key_up": [
{
"set_variable": {
"name": "shift_held",
"value": 0
}
}
],
"type": "basic"
},
{
"from": { "key_code": "right_shift" },
"to": [
{
"set_variable": {
"name": "shift_held",
"value": 1
}
},
{ "key_code": "right_shift" }
],
"to_after_key_up": [
{
"set_variable": {
"name": "shift_held",
"value": 0
}
}
],
"type": "basic"
}
]
}
Here, I resorted to AI to help get this put in place. Even then, there was a deal of toing and froing before the setup worked well. After that, it was time to get the quote (") and at (@) symbols assigned to what I was used to having on a British English keyboard:
{
"description": "Swap @ and \" keys (Shift+2 and Shift+quote)",
"manipulators": [
{
"from": {
"key_code": "2",
"modifiers": {
"mandatory": ["shift"],
"optional": ["any"]
}
},
"to": [
{
"key_code": "quote",
"modifiers": ["shift"]
}
],
"type": "basic"
},
{
"from": {
"key_code": "quote",
"modifiers": {
"mandatory": ["shift"],
"optional": ["any"]
}
},
"to": [
{
"key_code": "2",
"modifiers": ["shift"]
}
],
"type": "basic"
}
]
}
The above possibly was one of the first changes that I made, and took less time than some of the others that came after it. There was another at the end that was even simpler again: neutralising the Caps Lock key. That came up while I was perusing the Karabiner-Elements website, so here it is:
{
"manipulators": [
{
"description": "Change caps_lock to command+control+option+shift.",
"from": {
"key_code": "caps_lock",
"modifiers": { "optional": ["any"] }
},
"to": [
{
"key_code": "left_shift",
"modifiers": ["left_command", "left_control", "left_option"]
}
],
"type": "basic"
}
]
}
That was the simplest of the lot to deploy, being a simple copy and paste effort. It also halted mishaps when butter-fingered actions on the keyboard activated capitals when I did not need them. While there are occasions when the facility would have its uses, it has not noticed its absence since putting this in place.
At the end of all the tinkering, I now have a set-up that works well for me. While possible enhancements may include changing the cursor positioning and corresponding highlighting behaviours, I am happy to leave these aside for now. Compatibly with British and Irish keyboards together with smoother working in remote sessions was what I sought, and I largely have that. Thus, I have no complaints so far.
Please be aware that comment moderation is enabled and may delay the appearance of your contribution.