This project provides a Vim-mode standalone or plugin config for kanata.
It brings Vim motions, actions, and modes to any text field on any operating system.
This works by introducing extra keyboard layers where keys are mapped to OS native keyboard shortcuts to navigate text.
As an example, switching to the Vim normal layer, the h j k l keys are now mapped to the arrow keys, w is mapped to Alt-Right, and so on.
This approach is inspired by the Karabiner-Elements Vim Mode Plus mod.
Demo Video:
screenrecording-2026-06-27_17-40-33.mp4
- About this config
- How to integrate in your kanata keymap
- Visualize vim mode
- Supported Vim movements and actions
The "plugin" consists of a collection of kanata .kbd config files that you can include in your kanata configuration.
vim-standalone.kbd: A ready to use config for running in standalone mode.1-interface.kbd: OS-specific primitive actions (@left,@end-of-word,@copy,@undo, ...). This is the only place that should use raw OS key combos.2-shared.kbd: higher-level combos/macros (@delete-word,@copy-line,@paste,@replace, ...).3-bootstrapping.kbd: mode state machine and entrypoint templates.4-normal-layer.kbd: Vim normal mode keymap.5-visual-layer.kbd: Vim visual mode keymap.6-visual-line-layer.kbd: Vim visual-line mode keymap.
- The plugin orchestrates 4 logical modes/layers:
insert,vim-normal,vim-visual,vim-visual-line. - For each mode, it also activates a respective override layer:
insert-override,vim-normal-override,vim-visual-override,vim-visual-line-override. - Override layers let you add custom remaps while reusing the shared Vim logic.
There are two options on how to integrate this config into your setup:
- Standalone mode -> run a dedicated kanata instance (Recommended)
- Plugin mode -> add the config to your existing kanata config
If you are running on Linux, I'd always recommend this approach.
On other OSes, this will only work if you have no custom kanata setup.
The idea is to have a dedicated kanata instance running that is solely responsible for handling the Vim modes.
This kanata instance can be applied to external keyboards (e.g. running on ZMK), but also on top of other kanata instances (Linux only?!).
Simply run a dedicated kanata process with a config similar to the one in vim-standalone.kbd.
To run a kanata on top of another kanata instance, I've found these two ways, which only seem to be supported on Linux:
- Via the keyboard name
- use(defcfg linux-output-device-name "my-kanata-instance")on the first instance to set a unique name
- use(defcfg linux-dev-names-include ("my-kanata-instance"))on the kanata Vim instance to apply on top of the first kanata instance - Via the keyboard path
- usekanata --symlink-path /some/path/my-kanata-instanceto start first instance
- use(defcfg linux-dev /some/path/my-kanata-instance)on the kanata Vim instance to apply on top of the first kanata instance
Running a dedicated kanata instance allows to fully decouple the setup of the Vim config from any custom keymaps.
So all custom keymap quirks such as homerow mods will simply work out of the box when in Vim mode.
This approach injects the Vim config into an existing kanata setup.
It usually requires a bit more effort to make sure all keybindings work correctly.
Include the vim plugin files in this strict order at the top of your keymap.
(include kanata-vim/1-interface.kbd)
(include kanata-vim/2-shared.kbd)
(include kanata-vim/3-bootstrapping.kbd)
(include kanata-vim/4-normal-layer.kbd)
(include kanata-vim/5-visual-layer.kbd)
(include kanata-vim/6-visual-line-layer.kbd)
Then wire your own config.
Minimal plugin-relevant settings (optional):
(defcfg
process-unmapped-keys yes
alias-to-trigger-on-load switch-insert
)
process-unmapped-keys yes: lets unmapped keys pass through while Vim layers are active.alias-to-trigger-on-load switch-insert: starts Kanata in insert mode.
You need:
- an
insertlayer (i.e. rename your normal typing layer to "insert") - these override layermaps (can be empty initially):
insert-overridevim-normal-overridevim-visual-overridevim-visual-line-override
Example empty scaffolding:
(deflayer insert <your custom keymap here>)
(deflayermap (insert-override))
(deflayermap (vim-normal-override))
(deflayermap (vim-visual-override))
(deflayermap (vim-visual-line-override))
In the examples below, the vim entrypoint is mapped to esc.
But of course you can map it to any key instead of esc, such as caps etc.
Map any key to the switch-vim-normal alias:
(deflayermap (insert)
esc @switch-vim-normal
)
Or get creative in how to enter vim mode. E.g. by double tap on escape:
(deflayermap (insert)
esc (tap-dance 200 (esc @switch-vim-normal))
)
It can be very nice to have a different vim mode trigger depending on the currently used app.
E.g. in the terminal I need my escape key to behave normally (for actual vim use).
But in my browser, I want the escape key to toggle the vim mode immediately.
You can use the vim-entrypoint template for such a setup:
(deflayermap (insert)
esc (t! vim-entrypoint rsft)
)
Behavior:
- tap
esc: normal key behavior - hold
right shift+ tapesc: entervim-normal - if virtual key
vim-direct-enteris active, always entersvim-normalon tapesc
This only makes sense in conjunction with an external script that toggles the vim-direct-enter virtual key based on the active app.
For an example script that works with Hyprland on Linux, refer to hypr-window-listener.lua or hypr-window-listener.sh.
The plugin implementations of the vim layers assume the default vim mappings.
E.g. in normal mode layer, the "j" key is mapped to the down arrow key.
If you're using homerow mods, you most likely want to map a long tap of "j" to the shift key.
Or maybe you're using caps as escape key and want to use it to exit normal mode.
This can easily be achieved by using the override layers without touching the Vim layer implementation.
Here are some simple examples (same principle applies to all override layers):
(deflayermap (vim-normal-override)
caps @switch-insert ;; exit normal mode by tapping the `caps` key.
esc (multi esc @switch-insert) ;; Exit normal, but also tap the "Escape" key for the outside to use
j (tap-hold 200 200 _ rsft) ;; add a homerow mod to normal mode (`_` will pass the keypress to the default vim normal layer action)
)
(include kanata-vim/1-interface.kbd)
(include kanata-vim/2-shared.kbd)
(include kanata-vim/3-bootstrapping.kbd)
(include kanata-vim/4-normal-layer.kbd)
(include kanata-vim/5-visual-layer.kbd)
(include kanata-vim/6-visual-line-layer.kbd)
(defcfg
process-unmapped-keys yes
alias-to-trigger-on-load switch-insert
)
(defsrc
esc
caps
j
k
;; ... all other physical keys
)
(deflayer insert
esc
caps (t! vim-entrypoint rsft)
f (tap-hold 200 200 f lsft)
j (tap-hold 200 200 j rsft)
;; ... all other mappings
)
(deflayermap (insert-override)
)
(deflayermap (vim-normal-override)
esc (multi esc @switch-insert)
caps (multi esc @switch-insert)
f (tap-hold 200 200 _ rsft)
j (tap-hold 200 200 _ rsft)
)
(deflayermap (vim-visual-override)
esc (multi @deselect @switch-vim-normal)
caps (multi @deselect @switch-vim-normal)
)
(deflayermap (vim-visual-line-override)
esc (multi @deselect @switch-vim-normal)
caps (multi @deselect @switch-vim-normal)
)
It can be very confusing to use this plugin without a way to see which vim mode is currently active.
Luckily the kanata TCP port allows to build simple integrations with external tools.
For example, refer to quickshell-kanata-vim-status waybar-kanata-vim-status.sh for a simple integration with Linux Quickshell or Waybar.
(A more simple approach is to activate CAPS mode while vim layers are active - this shows a small caps-indicator on some OSes.)
| Key / Combo | Action | Normal | Visual | Visual-line |
|---|---|---|---|---|
h, j, k, l |
Move/select left down up right | ✅ | ✅ | ✅ |
b |
Move/select to start of word | ✅ | ✅ | |
w, e |
Move/select to end of word | ✅ | ✅ | |
0, _ |
Move/select to start of line | ✅ | ✅ | |
$ |
Move/select to end of line | ✅ | ✅ | |
gg |
Go/select to start of document | ✅ | ✅ | ✅ |
G |
Go/select to end of document | ✅ | ✅ | ✅ |
i, a |
Enter insert mode | ✅ | ||
I |
Insert at start of line | ✅ | ||
A |
Append at end of line | ✅ | ||
o |
Open line below | ✅ | ||
O |
Open line above | ✅ | ||
x |
Delete selection/character | ✅ | ✅ | ✅ |
X |
Delete previous character | ✅ | ||
d |
Delete selection | ✅ | ✅ | |
dd |
Delete line | ✅ | ||
D |
Delete to end of line | ✅ | ||
diw, daw |
Delete word | ✅ | ||
cc |
Change line | ✅ | ||
C |
Change to end of line | ✅ | ||
ciw, caw |
Change inner word | ✅ | ||
y |
Yank/copy selection | ✅ | ✅ | |
yy |
Yank/copy line | ✅ | ||
Y |
Yank/copy to end of line | ✅ | ||
yiw, yaw |
Yank/copy word | ✅ | ||
v |
Enter visual mode | ✅ | ||
V |
Enter visual-line mode | ✅ | ||
viw, vaw |
Select word | ✅ | ||
p |
Paste | ✅ | ✅ | ✅ |
P |
Paste above | ✅ | ✅ | ✅ |
r |
Replace one character | ✅ | ||
u |
Undo | ✅ | ||
C-r |
Redo | ✅ | ||
C-d |
Half-page down | ✅ | ✅ | ✅ |
C-u |
Half-page up | ✅ | ✅ | ✅ |
. |
Repeat last action (limited support) | ✅ | ||
Esc |
Exit to normal / insert mode | ✅ | ✅ | ✅ |