post 9 min read
Building a Keyboard-First Blog
How I built a keyboard-first blog with Vim motions, from the keymap to the accessibility off switch
- vim
- web
- ux
The missing keyboard layer
The web has evolved in extraordinary ways, yet there’s one thing it still hasn’t quite figured out: KEYBOARD-FIRST INTERFACES.
Keyboard-first browser extensions already exist, but they’re missing something: a truly native experience, designed and tailored to each page.
So I decided to do things differently. Since I was building this blog anyway, I figured: “why not bring that experience to my own corner of the web?”
The case for Vim
Vim saves you about 30 minutes of typing per year. Of course, that is later taxed by the two hours you spend evangelizing it to fellow developers who have not made the switch yet.
The premise is simple: “Your hands shall not leave the keyboard.”
And it has a good justification. Just look at the numbers:
Do the math: an 8-hour workday saves roughly 8 minutes just by keeping your hands on the keyboard. Assuming you are only typing and using the mouse, of course.
Building the keyboard layer
The core of the problem comes from my own experience, so it only makes sense that the solution starts with a look at user experience (UX).
Fortunately, last year I took a Human-Computer Interaction course, where I was first introduced to Nielsen’s 10 usability heuristics.
The two heuristics that mattered
I met all ten in that course. Here, two stood out.
Flexibility and Efficiency of Use says shortcuts should speed up a familiar route, not replace it. Match Between the System and the Real World says to borrow a vocabulary readers may already know.
For a developer blog, Vim was the obvious reference. Stack Overflow’s 2026 survey has Vim at 27.2% and Neovim at 14.7%, with overlap: enough to borrow from, not to assume. The mouse, Tab, and visible controls stay.
Scrolling motions
Scrolling was the easy import. j and k move the page down and up, gg and G jump to the ends, and d/u move half a page.
Vim uses Ctrl+D and Ctrl+U for that last pair. Browsers already use Ctrl+D to bookmark a page and Ctrl+U to view its source, so I dropped the Ctrl. The motions stay, and the browser keeps its shortcuts.
export const LINE = 64;
export const scroll = {
lines(count: number): void {
window.scrollBy({
top: count * LINE,
behavior: scrollBehavior(),
});
},
halfPage(direction: Direction): void {
window.scrollBy({
top: direction * Math.round(window.innerHeight / 2),
behavior: scrollBehavior(),
});
},
};
The default is smooth. Readers who ask for reduced motion get instant. Holding the key starts a small animation loop that ramps up the speed; releasing stops it.
Choosing a navigation zone
Navigating HTML elements is a different problem. Blog posts, the navbar, and social links are not lines of text, so I considered two approaches.
- A key for everything. Give every important element its own shortcut, the way Vim-style extensions do:
vkfor the archive,vgfor about,fafor a post. It is the fastest path for a power user, but it asks you to memorize a dictionary for a page you might visit twice. This is not Excel; a blog does not need a shortcut for every command. - Improve Tab. Browsers already have Tab, which walks the tabindex. The problem is scope. On a normal page, reaching the social links might take 20 presses. Technically keyboard-first, practically unusable.
I went with the second option, with one change: choose the zone before you cycle through it. Navigation, social links, content, posts. Instead of one long Tab chain, you get short loops that wrap around.
Why n and p won
The next question was: which keys should move within a zone, and which should move between them?
Vim has a motion for every unit: h/l for characters, w/e/b for words, j/k for lines, {/} for paragraphs. For larger places, Ctrl+W switches windows and :cnext/:cprev walks the buffer list. A post is not a character, word, or line. It could be a paragraph, but a buffer was the closest fit, so posts became buffers and zones became windows.
My first version mixed two vocabularies: posts got n/p, everything else got j/k/h/l. Since :cnext/:cprev are too long to type, I borrowed the popular <leader>n/<leader>p remap. Without a leader key here, I dropped the prefix.
It gave the same page two languages for navigation. I kept catching myself pressing the wrong keys, and I built the thing. If I couldn’t keep it straight, nobody else would.
So I unified everything on n/p. Three things settled it:
j/kalready scrolled the page, and reusing them would give the same keys two jobs.- The navbar, social links, and posts are all collections of HTML elements.
n/palready worked for posts and make sense to someone who has never opened Vim.
That was cleaner, so I stuck with it.
Then came the catch: I couldn’t assume Vim users shared the remap. So I tried the more conventional w/e forward and b back, treating posts as words.
In practice, it was miserable: w and e moved down, b moved up, and my brain refused to cooperate. I gave it a genuine try and went straight back to n/p.
Moving between panes
There was one more gap: how do you get into a zone in the first place?
Vim switches windows with Ctrl+W plus a direction. That is three keys to move between two regions. On a blog, the regions are just the sidebar and the page. So w enters the sidebar navigation, and w again drops you out. I kept the w from Ctrl+W; the Ctrl and the direction keys were doing no work here.
I would have preferred n for “navigation”, but n was already busy being “next”. w is the compromise.
Search and the remaining keys
Search is /, as in Vim, with Ctrl+K as an alternative for people who live in command palettes. Chrome and Firefox already use it to focus search, and GitHub, Linear, and Slack use it too: the muscle memory is already there.
The remaining keys are not Vim. They cannot be; they are site-specific ideas, not editing motions. So I picked the first letter and moved on:
ttoggles the themesfocuses the social linksoopens and collapses the sidebar?opens the shortcut list
That last one does more work than it looks. Recognition over recall: nobody is born knowing my keymap, and even a Vim user will need a map for the parts that are not Vim.
Here is the final map:
On a listing page, n and p walk the visible items. On a post page, they go straight to the older or newer post. Same keys, different list.
Then there is the implementation detail that keeps the whole thing honest: the keymap is a single source of truth. The handlers and the ? dialog read from the same object, so the documentation cannot drift from the behavior.
export const keymap = {
// movement
next: ['n'],
previous: ['p'],
sidebarNav: ['w'],
sidebarSocial: ['s'],
sidebarToggle: ['o'],
// reading
scrollDown: ['j'],
scrollUp: ['k'],
halfPageDown: ['d'],
halfPageUp: ['u'],
jumpTop: ['g'],
jumpBottom: ['G'],
// global
search: ['/'],
shortcuts: ['?'],
theme: ['t'],
} as const;
This is an excerpt; the real file also handles Esc, q, and the Ctrl+K variant.
The accessibility off switch
While auditing the result, I found a requirement I had not considered: WCAG 2.1.4, Character Key Shortcuts. If a shortcut uses a single letter, number, or punctuation key, at least one of these must be true:
- It can be turned off.
- It can be remapped.
- It only works while the relevant component has focus.
A site full of single-key shortcuts needs an off switch. So the sidebar now has one, next to the theme control. It is also why the handler ignores keys while you are typing in a field or when a dialog is open: a / in the search box should not open a second search box.
[ note ]
WCAG gives three options, and the toggle only covers one of them. A remap screen would be more surface area than a personal blog deserves. An off switch is the option that actually fits the site, and the one a reader can understand without reading the docs.
What I got out of it
That is the whole system: a keyboard-first website that still works if you never touch the keyboard layer. And this post is the demo. Press ? to see the full map, or just j and k to keep reading. If the shortcuts get in your way, the off switch is in the sidebar.
I know my blog will never have a crowd of power users. I used this as an excuse to study new concepts, especially in UX, and to work toward something I would love to see standardized across the web.
It probably cannot be. A general set of shortcuts makes little sense when every website has a completely different layout. But well, I did my part.
References
- Keyboard shortcuts vs. mouse: Omanson, Miller, Young & Schwantes (2010), Comparison of Mouse and Keyboard Efficiency, HFES. Citing Card, Moran & Newell (1983, p. 264) for the 0.2s/1.3s constants. https://journals.sagepub.com/doi/10.1177/154193121005400612
- Keyboard-mouse switching: Burns, Ritter & Zhang (2016), Using Naturalistic Typing to Update Architecture Typing Constants, ICCM 2016. https://acs.ist.psu.edu/iccm2016/proceedings/burns2016iccm.pdf
- Vim usage: Stack Overflow Developer Survey (2026), “Dev IDEs”, n = 13,193. https://survey.stackoverflow.co/2026/technology/data/dev-ide
- Heuristics: Nielsen, J. (1994), 10 Usability Heuristics for User Interface Design, Nielsen Norman Group. https://www.nngroup.com/articles/ten-usability-heuristics/
- Character key shortcuts: W3C, Understanding Success Criterion 2.1.4: Character Key Shortcuts, WCAG 2.2. https://www.w3.org/WAI/WCAG22/Understanding/character-key-shortcuts.html