a/

← Writing

Shipping Browser Shortcuts

I set out to build a real, multi-framework shortcuts library, not a side hook. I wrote the React core myself, then used Claude Code to take it to six frameworks fast, including the bugs it found in its own work and the npm publishing gauntlet that followed.

September 2026 · 7 min read

Full disclosure, right up top: I wrote the original React hook myself, by hand, no assistance. Everything past that, turning it into a proper package, generalising it to five more frameworks, writing the test suite, and hunting down the bugs below, was done with Claude Code. I am not burying that in a footnote. It is the actual subject of this post.

Built big on purpose

This did not start as a weekend hack that accidentally grew legs. I sat down wanting to build a real shortcuts library, the kind of thing that could declare keyboard, mouse, and wheel shortcuts as plain strings, things like "CTRL+SHIFT+F", "CMD+Z", "DOUBLE_LEFT_BUTTON", and wire them up to handlers without a single hand-rolled event listener anywhere in your app. I built the React core myself, from scratch. That part was always going to be React-only in my head, at first, a solid foundation before anything else.

"Something big" was the goal from day one, not an afterthought, and a shortcuts library that only works in React is not big, it is a hook with a README. So rather than spend weeks personally relearning the lifecycle quirks of five frameworks I do not use day to day, I brought in Claude Code and pointed it at the goal directly: take this React implementation and turn it into a real, multi-framework package. Quickly. What came back, and how fast it came back, is the actual subject of this post.

One core, six costumes

The instinct when you want to support more frameworks is to go rewrite the library six times. Claude Code resisted that, thankfully, since six slightly different implementations means six slightly different bugs, forever. Instead it pulled the entire event-listening logic out of my original hook, the part that listens on the window, normalises a keyboard event into a string like "CTRL+SHIFT+F", and dispatches it to listeners, into a plain class with no framework opinions at all.

Every framework adapter is now that class wearing a costume appropriate to its house party. React kept my original context provider and hook, rebuilt on the shared core. Vue got a plugin and a composable. Svelte got a setup function plus a hook that reads its own context. Solid got a provider built on its own context primitive. Angular got an injectable singleton service with destroy-ref powered cleanup. Preact got, and I want you to sit with how funny this is, the exact same code as React, because its hooks API is basically a very polite tribute act.

The bugs were the fun part

Claude Code wrote a test suite that exercises the core and all six adapters: mount, fire the shortcut, unmount, confirm it stopped firing. Thirty two tests. Along the way that suite turned up two bugs in its own freshly written code, which it then diagnosed and fixed without me touching a line. Watching an AI catch and root cause its own bugs is a strange kind of satisfying, like watching someone proofread their own essay out loud and actually catch the typos.

Solid cleaned up a cleanup function that did not exist

The Solid provider called its cleanup hook to remove event listeners on unmount. It compiled fine, it ran fine, it just never actually cleaned up, no error most of the time, just a listener that outlived the component that created it. The cause: the test environment loaded the framework and its DOM renderer as two separate module instances, each with its own private copy of an internal ownership-tracking variable, so the cleanup registered against one copy while disposal tore down the other. Two universes, politely ignoring each other. The fix, since these components render no real DOM, was to stop going through the DOM renderer in tests entirely and drive everything through the framework core directly: one module, one owner, one universe.

Angular's tests demanded a hazing ritual

To test one injectable class, it needed the change detection engine, the JIT compiler, a dynamic testing platform, an explicit test-environment initialisation call, and a fight with the build tool over a brand new decorator transform before any of it would even parse. All of it went in, one dependency at a time, in the order the error messages demanded them. Angular testing is basically a boss fight with dependency injection, and I mean that as a compliment.

If your test suite does not surprise you at least once, you did not test hard enough, and it does not matter who, or what, wrote the tests.

Publishing: the real boss fight

Writing six framework adapters was, weirdly, the calm part of this project. The final boss was the npm publish command.

Round one

A flat permissions error demanding two-factor authentication or a token with a bypass flag enabled. Fine, I made a token with bypass explicitly enabled. Still failed.

Round two

Turned out the token I had carefully configured in the project was being silently ignored, because the package lives inside a workspace and npm refuses to read a workspace member’s own local config for auth. The token actually being used was a stale one sitting in my global config, created before I had ever heard the words "bypass two-factor".

Round three

Fixed the global token, got a brand new error: a not-found response on a package name that definitely existed. The new token had been scoped to the wrong account context during setup. Once that was sorted, the actual publish took about four seconds. The CLI does not care how much of an emotional journey you just had.

If you are staring at your own single-framework library

Generalising a library across frameworks is genuinely more approachable than it looks from the outside, and it is a legitimately good use of an AI pair. The pattern of one core with thin adapters is well established, the failure modes are well documented across enough issues to be learnable, and the tests catch you the moment you drift. You do not need a grand unifying abstraction, you need one honest class that does the actual work, and the discipline to keep every adapter dumb, just lifecycle glue, nothing clever.

And write the tests, whoever is writing the code. Not because tests are virtuous, but because they are the fastest way to find out where the mental model of how a framework works is quietly wrong. Both bugs above were a gap between what was assumed and what was actually true, and they only surfaced because a test insisted on checking.

I wrote the React hook, Claude Code wrote the rest, and I am comfortable putting my name on both halves. It is live now as browser-shortcuts on npm, MIT licensed, with adapters for React, Preact, Vue, Svelte, Solid, and Angular.

More of what I have shipped

browser-shortcuts is not the first thing I have put on npm. If any of this was useful, vue3-form, vue3-authentication, bootstrap-vue-datatable and storagedotjs are on there too, and worth a search if you work in Vue.

Written from design reasoning and production experience. No schema, code or internal detail of any former employer’s system is reproduced here.