<?xml version="1.0" encoding="UTF-8"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom"><channel><title>Manuel Albuquerque — blog</title><description>A public archive of what I am learning and building. Project write-ups, technical decisions, and notes along the way.</description><link>https://blog.manuelalbuquerque.com</link><language>en</language><atom:link href="https://blog.manuelalbuquerque.com/rss.xml" rel="self" type="application/rss+xml"/><lastBuildDate>Sat, 14 Nov 2026 00:00:00 GMT</lastBuildDate><item><title>Building a Keyboard-First Blog</title><link>https://blog.manuelalbuquerque.com/posts/blog-with-motion</link><guid isPermaLink="true">https://blog.manuelalbuquerque.com/posts/blog-with-motion</guid><description>&lt;h2&gt;The missing keyboard layer&lt;/h2&gt;
&lt;p&gt;The web has evolved in extraordinary ways, yet there&apos;s one thing it still hasn&apos;t quite figured out: &lt;em&gt;&lt;strong&gt;KEYBOARD-FIRST INTERFACES.&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Keyboard-first browser extensions already exist, but they&apos;re missing something: a truly native experience, designed and tailored to each page.&lt;/p&gt;
&lt;p&gt;So I decided to do things differently. Since I was building this blog anyway, I figured: &lt;em&gt;&amp;quot;why not bring that experience to my own corner of the web?&amp;quot;&lt;/em&gt;&lt;/p&gt;
&lt;hr&gt;
&lt;h2&gt;The case for Vim&lt;/h2&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;The premise is simple: &lt;em&gt;&lt;strong&gt;&amp;quot;Your hands shall not leave the keyboard.&amp;quot;&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;And it has a good justification. Just look at the numbers:&lt;/p&gt;
&lt;div class=&quot;my-8 border border-line/60 bg-surface/60&quot;&gt;
&lt;div class=&quot;border-b border-line/60 px-4 py-2.5&quot;&gt;
&lt;span class=&quot;label-caps text-fg-strong&quot;&gt;[ the evidence ]&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&quot;grid grid-cols-1 gap-px bg-line/60 sm:grid-cols-2&quot;&gt;
&lt;div class=&quot;bg-bg p-4 sm:p-5&quot;&gt;
&lt;span class=&quot;block font-mono text-2xl leading-none text-fg-strong&quot;&gt;3×&lt;/span&gt;
&lt;span class=&quot;label-caps mt-2 block text-[10px] text-muted&quot;&gt;Faster with keyboard shortcuts&lt;/span&gt;
&lt;span class=&quot;mt-2 block text-sm leading-relaxed text-fg&quot;&gt;A two-key shortcut takes &lt;strong class=&quot;font-medium text-fg-strong&quot;&gt;0.4 seconds&lt;/strong&gt;; moving the mouse and clicking, &lt;strong class=&quot;font-medium text-fg-strong&quot;&gt;1.3 seconds&lt;/strong&gt;.&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&quot;bg-bg p-4 sm:p-5&quot;&gt;
&lt;span class=&quot;block font-mono text-2xl leading-none text-fg-strong&quot;&gt;54&lt;/span&gt;
&lt;span class=&quot;label-caps mt-2 block text-[10px] text-muted&quot;&gt;Keyboard-mouse switches per hour&lt;/span&gt;
&lt;span class=&quot;mt-2 block text-sm leading-relaxed text-fg&quot;&gt;&lt;strong class=&quot;font-medium text-fg-strong&quot;&gt;53.6 hand switches&lt;/strong&gt; between keyboard and mouse per hour.&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h2&gt;Building the keyboard layer&lt;/h2&gt;
&lt;p&gt;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).&lt;/p&gt;
&lt;p&gt;Fortunately, last year I took a Human-Computer Interaction course, where I was first introduced to &lt;a href=&quot;https://www.nngroup.com/articles/ten-usability-heuristics/&quot;&gt;Nielsen&apos;s 10 usability heuristics&lt;/a&gt;.&lt;/p&gt;
&lt;h3&gt;The two heuristics that mattered&lt;/h3&gt;
&lt;p&gt;I met all ten in that course. Here, two stood out.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Flexibility and Efficiency of Use&lt;/strong&gt; says shortcuts should speed up a familiar route, not replace it. &lt;strong&gt;Match Between the System and the Real World&lt;/strong&gt; says to borrow a vocabulary readers may already know.&lt;/p&gt;
&lt;p&gt;For a developer blog, Vim was the obvious reference. &lt;a href=&quot;https://survey.stackoverflow.co/2026/technology/data/dev-ide&quot;&gt;Stack Overflow&apos;s 2026 survey&lt;/a&gt; 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.&lt;/p&gt;
&lt;h3&gt;Scrolling motions&lt;/h3&gt;
&lt;p&gt;Scrolling was the easy import. &lt;code&gt;j&lt;/code&gt; and &lt;code&gt;k&lt;/code&gt; move the page down and up, &lt;code&gt;gg&lt;/code&gt; and &lt;code&gt;G&lt;/code&gt; jump to the ends, and &lt;code&gt;d&lt;/code&gt;/&lt;code&gt;u&lt;/code&gt; move half a page.&lt;/p&gt;
&lt;p&gt;Vim uses &lt;code&gt;Ctrl+D&lt;/code&gt; and &lt;code&gt;Ctrl+U&lt;/code&gt; for that last pair. Browsers already use &lt;code&gt;Ctrl+D&lt;/code&gt; to bookmark a page and &lt;code&gt;Ctrl+U&lt;/code&gt; to view its source, so I dropped the Ctrl. The motions stay, and the browser keeps its shortcuts.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;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(),
        });
    },
};
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h3&gt;Choosing a navigation zone&lt;/h3&gt;
&lt;p&gt;Navigating HTML elements is a different problem. Blog posts, the navbar, and social links are not lines of text, so I considered two approaches.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;A key for everything.&lt;/strong&gt; Give every important element its own shortcut, the way Vim-style extensions do: &lt;code&gt;vk&lt;/code&gt; for the archive, &lt;code&gt;vg&lt;/code&gt; for about, &lt;code&gt;fa&lt;/code&gt; for 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.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Improve Tab.&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;I went with the second option, with one change: &lt;strong&gt;choose the zone before you cycle through it.&lt;/strong&gt; Navigation, social links, content, posts. Instead of one long Tab chain, you get short loops that wrap around.&lt;/p&gt;
&lt;h3&gt;Why n and p won&lt;/h3&gt;
&lt;p&gt;The next question was: which keys should move within a zone, and which should move between them?&lt;/p&gt;
&lt;p&gt;Vim has a motion for every unit: &lt;code&gt;h&lt;/code&gt;/&lt;code&gt;l&lt;/code&gt; for characters, &lt;code&gt;w&lt;/code&gt;/&lt;code&gt;e&lt;/code&gt;/&lt;code&gt;b&lt;/code&gt; for words, &lt;code&gt;j&lt;/code&gt;/&lt;code&gt;k&lt;/code&gt; for lines, &lt;code&gt;{&lt;/code&gt;/&lt;code&gt;}&lt;/code&gt; for paragraphs. For larger places, &lt;code&gt;Ctrl+W&lt;/code&gt; switches windows and &lt;code&gt;:cnext&lt;/code&gt;/&lt;code&gt;:cprev&lt;/code&gt; 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.&lt;/p&gt;
&lt;p&gt;My first version mixed two vocabularies: posts got &lt;code&gt;n&lt;/code&gt;/&lt;code&gt;p&lt;/code&gt;, everything else got &lt;code&gt;j&lt;/code&gt;/&lt;code&gt;k&lt;/code&gt;/&lt;code&gt;h&lt;/code&gt;/&lt;code&gt;l&lt;/code&gt;. Since &lt;code&gt;:cnext&lt;/code&gt;/&lt;code&gt;:cprev&lt;/code&gt; are too long to type, I borrowed the popular &lt;code&gt;&amp;lt;leader&amp;gt;n&lt;/code&gt;/&lt;code&gt;&amp;lt;leader&amp;gt;p&lt;/code&gt; remap. Without a leader key here, I dropped the prefix.&lt;/p&gt;
&lt;div class=&quot;my-8 border border-line/60 bg-surface/60&quot;&gt;
&lt;div class=&quot;flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1 border-b border-line/60 px-4 py-2.5&quot;&gt;
&lt;span class=&quot;label-caps text-fg-strong&quot;&gt;[ attempt 1 ]&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&quot;grid grid-cols-1 gap-px bg-line/60 sm:grid-cols-3&quot;&gt;
&lt;div class=&quot;bg-bg px-4 py-3&quot;&gt;
&lt;span class=&quot;label-caps block text-[10px] text-muted&quot;&gt;sidebar&lt;/span&gt;
&lt;span class=&quot;mt-1 block font-mono text-sm text-fg-strong&quot;&gt;j / k / h / l&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&quot;bg-bg px-4 py-3&quot;&gt;
&lt;span class=&quot;label-caps block text-[10px] text-muted&quot;&gt;content&lt;/span&gt;
&lt;span class=&quot;mt-1 block font-mono text-sm text-fg-strong&quot;&gt;n / p&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&quot;bg-bg px-4 py-3&quot;&gt;
&lt;span class=&quot;label-caps block text-[10px] text-muted&quot;&gt;what broke&lt;/span&gt;
&lt;span class=&quot;mt-1 block text-sm text-fg&quot;&gt;Two languages for the same idea&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;It gave the same page two languages for navigation. I kept catching myself pressing the wrong keys, and I &lt;em&gt;built&lt;/em&gt; the thing. If I couldn&apos;t keep it straight, nobody else would.&lt;/p&gt;
&lt;p&gt;So I unified everything on &lt;code&gt;n&lt;/code&gt;/&lt;code&gt;p&lt;/code&gt;. Three things settled it:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;code&gt;j&lt;/code&gt;/&lt;code&gt;k&lt;/code&gt; already scrolled the page, and reusing them would give the same keys two jobs.&lt;/li&gt;
&lt;li&gt;The navbar, social links, and posts are all collections of HTML elements.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;n&lt;/code&gt;/&lt;code&gt;p&lt;/code&gt; already worked for posts and make sense to someone who has never opened Vim.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;That was cleaner, so I stuck with it.&lt;/p&gt;
&lt;div class=&quot;my-8 border border-line/60 bg-surface/60&quot;&gt;
&lt;div class=&quot;flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1 border-b border-line/60 px-4 py-2.5&quot;&gt;
&lt;span class=&quot;label-caps text-fg-strong&quot;&gt;[ attempt 2 ]&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&quot;grid grid-cols-1 gap-px bg-line/60 sm:grid-cols-3&quot;&gt;
&lt;div class=&quot;bg-bg px-4 py-3&quot;&gt;
&lt;span class=&quot;label-caps block text-[10px] text-muted&quot;&gt;sidebar&lt;/span&gt;
&lt;span class=&quot;mt-1 block font-mono text-sm text-fg-strong&quot;&gt;n / p&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&quot;bg-bg px-4 py-3&quot;&gt;
&lt;span class=&quot;label-caps block text-[10px] text-muted&quot;&gt;content&lt;/span&gt;
&lt;span class=&quot;mt-1 block font-mono text-sm text-fg-strong&quot;&gt;n / p&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&quot;bg-bg px-4 py-3&quot;&gt;
&lt;span class=&quot;label-caps block text-[10px] text-muted&quot;&gt;what broke&lt;/span&gt;
&lt;span class=&quot;mt-1 block text-sm text-fg&quot;&gt;n/p is a personal remap&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;Then came the catch: I couldn&apos;t assume Vim users shared the remap. So I tried the more conventional &lt;code&gt;w&lt;/code&gt;/&lt;code&gt;e&lt;/code&gt; forward and &lt;code&gt;b&lt;/code&gt; back, treating posts as words.&lt;/p&gt;
&lt;div class=&quot;my-8 border border-line/60 bg-surface/60&quot;&gt;
&lt;div class=&quot;flex flex-wrap items-baseline justify-between gap-x-4 gap-y-1 border-b border-line/60 px-4 py-2.5&quot;&gt;
&lt;span class=&quot;label-caps text-fg-strong&quot;&gt;[ attempt 3 ]&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&quot;grid grid-cols-1 gap-px bg-line/60 sm:grid-cols-3&quot;&gt;
&lt;div class=&quot;bg-bg px-4 py-3&quot;&gt;
&lt;span class=&quot;label-caps block text-[10px] text-muted&quot;&gt;sidebar&lt;/span&gt;
&lt;span class=&quot;mt-1 block font-mono text-sm text-fg-strong&quot;&gt;w / e / b&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&quot;bg-bg px-4 py-3&quot;&gt;
&lt;span class=&quot;label-caps block text-[10px] text-muted&quot;&gt;content&lt;/span&gt;
&lt;span class=&quot;mt-1 block font-mono text-sm text-fg-strong&quot;&gt;w / e / b&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&quot;bg-bg px-4 py-3&quot;&gt;
&lt;span class=&quot;label-caps block text-[10px] text-muted&quot;&gt;what broke&lt;/span&gt;
&lt;span class=&quot;mt-1 block text-sm text-fg&quot;&gt;Sideways in the sidebar&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;In practice, it was miserable: &lt;code&gt;w&lt;/code&gt; and &lt;code&gt;e&lt;/code&gt; moved down, &lt;code&gt;b&lt;/code&gt; moved up, and my brain refused to cooperate. I gave it a genuine try and went straight back to &lt;code&gt;n&lt;/code&gt;/&lt;code&gt;p&lt;/code&gt;.&lt;/p&gt;
&lt;div class=&quot;my-8 border border-line/60 bg-surface/60 p-4 sm:p-5&quot;&gt;
&lt;span class=&quot;block text-lg font-medium leading-snug text-fg-strong sm:text-xl&quot;&gt;This is a website, not a text editor.&lt;/span&gt;
&lt;span class=&quot;mt-1 block text-sm leading-relaxed text-muted&quot;&gt;Some things simply cannot be the same as in Vim.&lt;/span&gt;
&lt;/div&gt;
&lt;h3&gt;Moving between panes&lt;/h3&gt;
&lt;p&gt;There was one more gap: how do you get &lt;em&gt;into&lt;/em&gt; a zone in the first place?&lt;/p&gt;
&lt;p&gt;Vim switches windows with &lt;code&gt;Ctrl+W&lt;/code&gt; 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 &lt;code&gt;w&lt;/code&gt; enters the sidebar navigation, and &lt;code&gt;w&lt;/code&gt; again drops you out. I kept the &lt;code&gt;w&lt;/code&gt; from &lt;code&gt;Ctrl+W&lt;/code&gt;; the Ctrl and the direction keys were doing no work here.&lt;/p&gt;
&lt;p&gt;I would have preferred &lt;code&gt;n&lt;/code&gt; for &amp;quot;navigation&amp;quot;, but &lt;code&gt;n&lt;/code&gt; was already busy being &amp;quot;next&amp;quot;. &lt;code&gt;w&lt;/code&gt; is the compromise.&lt;/p&gt;
&lt;h3&gt;Search and the remaining keys&lt;/h3&gt;
&lt;p&gt;Search is &lt;code&gt;/&lt;/code&gt;, as in Vim, with &lt;code&gt;Ctrl+K&lt;/code&gt; 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.&lt;/p&gt;
&lt;p&gt;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:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;code&gt;t&lt;/code&gt; toggles the theme&lt;/li&gt;
&lt;li&gt;&lt;code&gt;s&lt;/code&gt; focuses the social links&lt;/li&gt;
&lt;li&gt;&lt;code&gt;o&lt;/code&gt; opens and collapses the sidebar&lt;/li&gt;
&lt;li&gt;&lt;code&gt;?&lt;/code&gt; opens the shortcut list&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;Here is the final map:&lt;/p&gt;
&lt;div class=&quot;my-8 border border-line/60 bg-surface/60&quot;&gt;
&lt;div class=&quot;overflow-x-auto bg-bg&quot;&gt;
&lt;div class=&quot;min-w-[34rem]&quot;&gt;
&lt;div class=&quot;grid grid-cols-[1.8fr_0.8fr_0.8fr_0.8fr] gap-4 border-b border-line/60 bg-surface/60 px-4 py-2.5&quot;&gt;
&lt;span class=&quot;label-caps text-[10px] text-muted&quot;&gt;zone&lt;/span&gt;
&lt;span class=&quot;label-caps text-[10px] text-muted&quot;&gt;get there&lt;/span&gt;
&lt;span class=&quot;label-caps text-[10px] text-muted&quot;&gt;move&lt;/span&gt;
&lt;span class=&quot;label-caps text-[10px] text-muted&quot;&gt;open&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&quot;divide-y divide-line/60&quot;&gt;
&lt;div class=&quot;grid grid-cols-[1.8fr_0.8fr_0.8fr_0.8fr] items-center gap-4 px-4 py-3&quot;&gt;
&lt;span class=&quot;text-sm font-medium text-fg-strong&quot;&gt;Sidebar navigation&lt;/span&gt;
&lt;span class=&quot;flex items-center gap-1&quot;&gt;&lt;kbd&gt;w&lt;/kbd&gt;&lt;/span&gt;
&lt;span class=&quot;flex items-center gap-1&quot;&gt;&lt;kbd&gt;n&lt;/kbd&gt;&lt;span class=&quot;text-xs text-muted&quot;&gt;/&lt;/span&gt;&lt;kbd&gt;p&lt;/kbd&gt;&lt;/span&gt;
&lt;span class=&quot;flex items-center gap-1&quot;&gt;&lt;kbd&gt;Enter&lt;/kbd&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&quot;grid grid-cols-[1.8fr_0.8fr_0.8fr_0.8fr] items-center gap-4 px-4 py-3&quot;&gt;
&lt;span class=&quot;text-sm font-medium text-fg-strong&quot;&gt;Social links&lt;/span&gt;
&lt;span class=&quot;flex items-center gap-1&quot;&gt;&lt;kbd&gt;s&lt;/kbd&gt;&lt;/span&gt;
&lt;span class=&quot;flex items-center gap-1&quot;&gt;&lt;kbd&gt;n&lt;/kbd&gt;&lt;span class=&quot;text-xs text-muted&quot;&gt;/&lt;/span&gt;&lt;kbd&gt;p&lt;/kbd&gt;&lt;/span&gt;
&lt;span class=&quot;flex items-center gap-1&quot;&gt;&lt;kbd&gt;Enter&lt;/kbd&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&quot;grid grid-cols-[1.8fr_0.8fr_0.8fr_0.8fr] items-center gap-4 px-4 py-3&quot;&gt;
&lt;span class=&quot;text-sm font-medium text-fg-strong&quot;&gt;Page content&lt;/span&gt;
&lt;span class=&quot;text-sm text-muted&quot;&gt;already there&lt;/span&gt;
&lt;span class=&quot;flex items-center gap-1&quot;&gt;&lt;kbd&gt;n&lt;/kbd&gt;&lt;span class=&quot;text-xs text-muted&quot;&gt;/&lt;/span&gt;&lt;kbd&gt;p&lt;/kbd&gt;&lt;/span&gt;
&lt;span class=&quot;flex items-center gap-1&quot;&gt;&lt;kbd&gt;Enter&lt;/kbd&gt;&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&quot;grid grid-cols-[1.8fr_0.8fr_0.8fr_0.8fr] items-center gap-4 px-4 py-3&quot;&gt;
&lt;span class=&quot;text-sm font-medium text-fg-strong&quot;&gt;Older / newer post&lt;/span&gt;
&lt;span class=&quot;text-sm text-muted&quot;&gt;already there&lt;/span&gt;
&lt;span class=&quot;flex items-center gap-1&quot;&gt;&lt;kbd&gt;n&lt;/kbd&gt;&lt;span class=&quot;text-xs text-muted&quot;&gt;/&lt;/span&gt;&lt;kbd&gt;p&lt;/kbd&gt;&lt;/span&gt;
&lt;span class=&quot;text-sm text-muted&quot;&gt;n/a&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&quot;grid grid-cols-[1.8fr_0.8fr_0.8fr_0.8fr] items-center gap-4 px-4 py-3&quot;&gt;
&lt;span class=&quot;text-sm font-medium text-fg-strong&quot;&gt;Show / hide sidebar&lt;/span&gt;
&lt;span class=&quot;flex items-center gap-1&quot;&gt;&lt;kbd&gt;o&lt;/kbd&gt;&lt;/span&gt;
&lt;span class=&quot;text-sm text-muted&quot;&gt;n/a&lt;/span&gt;
&lt;span class=&quot;text-sm text-muted&quot;&gt;n/a&lt;/span&gt;
&lt;/div&gt;
&lt;div class=&quot;grid grid-cols-[1.8fr_0.8fr_0.8fr_0.8fr] items-center gap-4 px-4 py-3&quot;&gt;
&lt;span class=&quot;text-sm font-medium text-fg-strong&quot;&gt;Exit navigation or a dialog&lt;/span&gt;
&lt;span class=&quot;flex items-center gap-1&quot;&gt;&lt;kbd&gt;Esc&lt;/kbd&gt;&lt;span class=&quot;text-xs text-muted&quot;&gt;/&lt;/span&gt;&lt;kbd&gt;q&lt;/kbd&gt;&lt;/span&gt;
&lt;span class=&quot;text-sm text-muted&quot;&gt;n/a&lt;/span&gt;
&lt;span class=&quot;text-sm text-muted&quot;&gt;n/a&lt;/span&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;p&gt;On a listing page, &lt;code&gt;n&lt;/code&gt; and &lt;code&gt;p&lt;/code&gt; walk the visible items. On a post page, they go straight to the older or newer post. Same keys, different list.&lt;/p&gt;
&lt;p&gt;Then there is the implementation detail that keeps the whole thing honest: the keymap is a single source of truth. The handlers and the &lt;code&gt;?&lt;/code&gt; dialog read from the same object, so the documentation cannot drift from the behavior.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;export const keymap = {
    // movement
    next: [&apos;n&apos;],
    previous: [&apos;p&apos;],
    sidebarNav: [&apos;w&apos;],
    sidebarSocial: [&apos;s&apos;],
    sidebarToggle: [&apos;o&apos;],

    // reading
    scrollDown: [&apos;j&apos;],
    scrollUp: [&apos;k&apos;],
    halfPageDown: [&apos;d&apos;],
    halfPageUp: [&apos;u&apos;],
    jumpTop: [&apos;g&apos;],
    jumpBottom: [&apos;G&apos;],

    // global
    search: [&apos;/&apos;],
    shortcuts: [&apos;?&apos;],
    theme: [&apos;t&apos;],
} as const;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This is an excerpt; the real file also handles &lt;code&gt;Esc&lt;/code&gt;, &lt;code&gt;q&lt;/code&gt;, and the &lt;code&gt;Ctrl+K&lt;/code&gt; variant.&lt;/p&gt;
&lt;h3&gt;The accessibility off switch&lt;/h3&gt;
&lt;p&gt;While auditing the result, I found a requirement I had not considered: &lt;a href=&quot;https://www.w3.org/WAI/WCAG22/Understanding/character-key-shortcuts.html&quot;&gt;WCAG 2.1.4, &lt;em&gt;Character Key Shortcuts&lt;/em&gt;&lt;/a&gt;. If a shortcut uses a single letter, number, or punctuation key, at least one of these must be true:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;It can be turned off.&lt;/li&gt;
&lt;li&gt;It can be remapped.&lt;/li&gt;
&lt;li&gt;It only works while the relevant component has focus.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;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 &lt;code&gt;/&lt;/code&gt; in the search box should not open a second search box.&lt;/p&gt;
&lt;div class=&quot;callout&quot;&gt;
&lt;p class=&quot;label-caps text-muted&quot;&gt;[ note ]&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;/div&gt;
&lt;h2&gt;What I got out of it&lt;/h2&gt;
&lt;p&gt;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 &lt;code&gt;?&lt;/code&gt; to see the full map, or just &lt;code&gt;j&lt;/code&gt; and &lt;code&gt;k&lt;/code&gt; to keep reading. If the shortcuts get in your way, the off switch is in the sidebar.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;p&gt;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.&lt;/p&gt;
&lt;h2&gt;References&lt;/h2&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Keyboard shortcuts vs. mouse:&lt;/strong&gt; Omanson, Miller, Young &amp;amp; Schwantes (2010), &lt;em&gt;Comparison of Mouse and Keyboard Efficiency&lt;/em&gt;, HFES. Citing Card, Moran &amp;amp; Newell (1983, p. 264) for the 0.2s/1.3s constants. &lt;a href=&quot;https://journals.sagepub.com/doi/10.1177/154193121005400612&quot;&gt;https://journals.sagepub.com/doi/10.1177/154193121005400612&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Keyboard-mouse switching:&lt;/strong&gt; Burns, Ritter &amp;amp; Zhang (2016), &lt;em&gt;Using Naturalistic Typing to Update Architecture Typing Constants&lt;/em&gt;, ICCM 2016. &lt;a href=&quot;https://acs.ist.psu.edu/iccm2016/proceedings/burns2016iccm.pdf&quot;&gt;https://acs.ist.psu.edu/iccm2016/proceedings/burns2016iccm.pdf&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Vim usage:&lt;/strong&gt; Stack Overflow Developer Survey (2026), &amp;quot;Dev IDEs&amp;quot;, n = 13,193. &lt;a href=&quot;https://survey.stackoverflow.co/2026/technology/data/dev-ide&quot;&gt;https://survey.stackoverflow.co/2026/technology/data/dev-ide&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Heuristics:&lt;/strong&gt; Nielsen, J. (1994), &lt;em&gt;10 Usability Heuristics for User Interface Design&lt;/em&gt;, Nielsen Norman Group. &lt;a href=&quot;https://www.nngroup.com/articles/ten-usability-heuristics/&quot;&gt;https://www.nngroup.com/articles/ten-usability-heuristics/&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Character key shortcuts:&lt;/strong&gt; W3C, &lt;em&gt;Understanding Success Criterion 2.1.4: Character Key Shortcuts&lt;/em&gt;, WCAG 2.2. &lt;a href=&quot;https://www.w3.org/WAI/WCAG22/Understanding/character-key-shortcuts.html&quot;&gt;https://www.w3.org/WAI/WCAG22/Understanding/character-key-shortcuts.html&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
</description><pubDate>Sat, 14 Nov 2026 00:00:00 GMT</pubDate><category>vim</category><category>web</category><category>ux</category></item></channel></rss>