Building Excel-style keyboard sequences for Google Sheets without turning the extension into a pile of DOM selectors
I recently shipped Spex, a Chrome extension that adds Excel-style sequential keyboard shortcuts and contextual KeyTips to Google Sheets. The user-facing interaction is intentionally small: - press Alt /Option - see the keys available at that moment - continue the sequence - run the selected Sheets command The implementation stopped being small as soon as I wanted it to behave like a real input system rather than a collection of keydown handlers. This post is about the architecture decisions that became important once the extension had to coexist with Google Sheets, cell editing, IME composition, focus management, and a host UI that can change independently of the extension. Spex site/demo: https://spex.kikuta.dev/en Chrome Web Store: https://chromewebstore.google.com/detail/gahnolkboklnjhnaahjdkingoejbepcc The obvious prototype A first version of this kind of extension can be surprisingly short: listen for keydown -> recognize shortcut -> query a target element -> click it That is enough to prove the idea. It is not enough to make the interaction reliable. Once I started testing real workflows, I had to answer questions such as: - What if the user is editing a cell? - What if an IME is in the middle of composition? - What if the UI language changes? - What if Google changes a selector? - What if the synthetic event is accepted but the intended state does not change? - What if a command opens a menu but leaves focus in the wrong place? - What happens when a delayed compositionend arrives after my own sequence has already completed? The shortcut map was not the hard part. The boundary between my input system and the host application's input system was. Separate shortcut semantics from host mechanics The core structure I ended up with is conceptually: Keyboard Event โ Core Shortcut Machine โ Semantic Command โ GoogleSheetsAdapter โ Google Sheets The shortcut machine should know things like: - the current sequence - which keys are valid next - whether Escape cancels the sequence - which semantic command has been selected It should not know: - a Google Sheets CSS selector - a translated menu label - where a toolbar button currently lives - how a particular command is verified So the core emits commands with meaning, for example: ToggleBold InsertRowAbove FreezeFirstRow OpenKeyboardShortcuts The adapter then decides how that command can be executed in the current Google Sheets UI. This separation matters because shortcut behavior and Google UI structure change for different reasons. I did not want a Sheets DOM change to force changes in the core shortcut model. One execution strategy is not enough Another early assumption I dropped was that every command should use the same execution mechanism. In practice, the reliable mechanism depends on the command. Spex uses three broad strategies. Stable DOM controls Some actions have a sufficiently stable host control that can be interacted with directly. Native Sheets shortcuts If Google Sheets already exposes a reliable native keyboard action, it can be better to use the host application's own interaction path rather than recreating it. Page-owned actions Some controls are not reliably activated through a simple synthetic click or key event, but the page owns an action-capable control that can be resolved at runtime. The rule I care about here is: do not build the integration around minified implementation names. If an action can only be found because an internal property happened to be called a.b.c in today's build, that is not a contract. Where Spex uses a page-owned action, the adapter tries to resolve it by capability. If the required control cannot be resolved, the command fails closed instead of guessing. A keyboard shortcut that occasionally does nothing is frustrating. A shortcut that claims success while doing the wrong thing is worse. Dispatch is not success This became one of the most important rules in the project. Suppose the user runs a bold command. From Spex's point of view, these are not equivalent: 1. I dispatched an event without throwing. 2. Google Sheets actually changed the bold state. The second one is the useful definition of success. So, where possible, the adapter verifies the result of the command before returning an executed state. For a formatting toggle, that can mean observing the control state. For an action that opens UI, it can mean checking that the expected UI became visible. For undo/redo or structure changes, the verification path is different again. This has two benefits: - failures are visible instead of becoming silent false positives - tests can assert user-visible outcomes instead of implementation details E2E against the real host application A mocked Google Sheets page is useful for many tests, but it cannot prove that a production content script still works against Google Sheets itself. The real-browser E2E path therefore checks behaviors such as: - Alt starts the KeyTips flow from a normal cell selection - a sequential command actually changes the corresponding Sheets state - the same sequence can be started again immediately after completion - real cell-editing mode keeps priority over the extension - IME/composition events do not leave the shortcut machine stuck - undo and menu-opening actions produce observable results The general lesson for me was that browser-extension integration tests need to cross the boundary that actually fails in production. If the risky dependency is the host application's behavior, testing only your own state machine gives a false sense of confidence. Keyboard-first software must know when not to own the keyboard This sounds obvious, but it is easy to get wrong. Spex is specifically trying to make more of Google Sheets accessible from the keyboard. That means it is also in a position to break the keyboard experience very badly. The extension should not steal Alt / Option while the user is genuinely editing content. It should not break Japanese IME composition. It should not suppress unrelated input events outside its own active sequence. It should recover if composition-related events arrive in an unexpected order. A useful mental model became: My shortcut system is a temporary mode requested by the user, not the owner of the page's input pipeline. That framing makes cancellation, focus preservation, composition handling, and failure recovery part of the primary feature rather than edge-case cleanup. KeyTips are a state visualization, not a shortcut cheat sheet The visible KeyTips are also tied to the state machine. They are not a giant static list of shortcuts. After Alt / Option , the user sees the keys available at the first level. After choosing a branch, the visible keys change to the commands available from that state. That matters for discoverability. A large shortcut catalog has a UX problem: the more shortcuts you add, the harder the product becomes to learn. Contextual KeyTips let the interaction scale without requiring the user to memorize the entire map in advance. Frequently used paths can become muscle memory. Rarely used paths remain discoverable on screen. Keep the product scope narrow Spreadsheet extensions have an obvious expansion path today: read the workbook, add AI, send data to a service, build an assistant around the document. I deliberately did not take that path with Spex. The product purpose is interaction: improve keyboard access to Google Sheets. Spex has no backend server receiving extension usage data and does not implement telemetry or analytics. Settings, onboarding state, and limited extension operational diagnostics are stored in Chrome extension local storage. Those diagnostics do not store spreadsheet URLs, cell values, formulas, or selection contents. This is partly a privacy decision, but it is also an architectural one. A narrow purpose makes it easier to reason about permissions, failure modes, product messaging, and long-term maintenance. A small secondary feature: dark mode that includes the grid Spex also includes a Google Sheets dark mode. The implementation goal was not simply to invert the entire page. Spreadsheet colors can carry meaning, so indiscriminate inversion can make a document misleading or unreadable. The practical requirement became: - include the actual sheet grid, because that is most of the screen - preserve authored colors as much as possible - prioritize text and border readability It is secondary to the keyboard system, but it follows the same product principle: remove a repeated interaction/visual friction without trying to reinterpret the spreadsheet itself. What I would reuse in another browser extension If I were building another extension on top of a large web application, I would reuse these rules: - Model intent as semantic commands before touching the host DOM. - Allow different commands to use different execution strategies. - Treat host internals as capabilities to resolve, not minified names to depend on. - Fail closed when the action cannot be resolved safely. - Verify user-visible outcomes instead of assuming dispatch means success. - Test against the real host application at the boundary that is most likely to break. - Treat focus, editing mode, and IME as core interaction states. - Make advanced keyboard actions discoverable instead of demanding memorization. None of these ideas are specific to spreadsheets. They apply to any extension trying to add a reliable interaction layer to a web app it does not control. Spex Spex is now available as a Chrome extension. - Site and demo: https://spex.kikuta.dev/en - Chrome Web Store: https://chromewebstore.google.com/detail/gahnolkboklnjhnaahjdkingoejbepcc - Privacy details: https://spex.kikuta.dev/en/privacy If you build browser extensions on top of complex web applications, I'd be interested in how you handle host UI churn and action verification. Those two problems ended up shaping far more of this project than the shortcut map itself. Top comments (0)
Comments
No comments yet. Start the discussion.