Lost Inputs
Posted
A UI shouldn’t lose your inputs.
- If you are typing, each and every key should result in a letter in order. No characters should e mssig or rerodeerd.
- If I switch windows with Alt+Tab and then type, the events must always go to the newly focused window.
- If I close a tab with Ctrl+w and then type, the events must go to the newly focused tab.
This may seem obvious, but I often see applications that behave incorrectly if your inputs come too fast. One common situation where I see interfaces falter is keyboard shortcuts that bring you to a text input. Keypresses after the shortcut should always land in that input.
Examples
App Launchers
A common example is an app launcher. On GNOME I tap Super and type to search my apps. If I type Superfoo it should search for “foo” every time. GNOME works correctly here. No matter how overloaded and laggy my system is, and no matter how fast I type, the search is accurately captured. KDE’s KRunner does not pass this test. Oftentimes the first few characters of my search will land in the previously-focused application. I think this is because it is implemented as a separate process, and it can take a moment to start up and capture keyboard focus. macOS’s Spotlight also fails, although the failure window seems much smaller than KDE’s and required significant system load to hit.
Clipboard
Ghostty handles paste events asynchronously. Typing Ctrl+Shift+vEnter can trigger the Enter first, then the pasted text lands on the next line.
Web Apps
This occurs on web apps too. I’ll pick on Linear as they appear to have thought about this to some degree. Some shortcuts like a1 will reliably self-assign the ticket no matter how fast I type or how locked up my system is. Even complex interactions like mbCtrl+VEnter will mark the current issue as blocked by the issue in my clipboard. That is how it should work!
However, they aren’t perfect. /foo can have various behaviours depending on how fast you type. It should search for “foo” every time, but I have seen a few failures:
- A prefix lands in the search box, but then it loses focus and future inputs are treated as global shortcuts again.
- Reordering such as “oof”. The “f” lands in the search box, but the cursor then resets to the start of the search box before I type “oo”.
Mouse Events
This can happen for mouse events too. For example Firefox has an excellent feature where after clicking the close button on a tab, the next tab’s close button will always land in the exact same spot. Even if the tabs were previously compressed because too many are open, they will wait until you move your mouse away to expand again. This way you can just keep clicking in the same location to close many tabs with ease.
But the tab closing animation ruins this! There is a gap in time where the old tab hasn’t gone away and clicks just disappear. So you can’t simply click 4 times to close 4 tabs, it may close fewer than you intended. You instead need to wait until the previous click is fully processed before performing the next click. This isn’t the worst failure mode but can make workflows where you close lots of tabs unnecessarily slow and annoying. Ctrl+W doesn’t appear to suffer this issue and always closes the right number of tabs.
This reminds me of the classic moving click target UX failure. This is the opposite of that: instead of the target jumping away as you try to click, the predictable target is instead not ready yet.
Difficulty
The main difficulty in implementing this correctly is that you need some sort of global input routing that buffers events until the application is ready for them. This can be quite difficult in browser applications as there is no way to “defer” input events other than completely blocking the main thread. So if follow-up keypresses are required you need to synchronously adjust focus, which may be difficult in your framework. This typically also precludes any form of lazy loading. Alternatively you would need to implement your own focus and input event handling logic, which is almost certainly a bad idea.
Asynchronous work is a complicated case in general, because buffering inputs for an extended period is confusing to the user. What if that work can fail? Do you drop the buffered input events? Or feed it to the previously focused input control? Do you need some sort of timeout? Luckily I find that this case is very rare. (Although the clipboard example above is one of these cases.)
If you have any sort of UI automation, this is easy to test: feed the keys in as a single batch and be sure that your app handles them properly!
Why?
Typing speed shouldn’t affect functionality! Keyboard shortcuts are designed for people to go fast, nothing will harm your productivity more than familiar keyboard shortcuts doing something unexpected! (Bonus points if those inputs aren’t lost, but trigger a completely different action like “delete”.) It is easy to say that it is a 2 millisecond gap and no one will hit a window that small, but that gap may be much larger on a low-end machine swapping like crazy. Or maybe my local LLM is hogging my GPU for a second or two. Don’t let fingers race your UI, just close the gap.