DEV Community

Ashraf
Ashraf

Posted on

Your App Doesn't Need a Rewrite. It Needs a Tab Key.

A post titled "GUIs should be fully keyboard-driven" hit 780 points and 395 comments on Hacker News this week. Not because it said anything radical. Because it said something obvious that almost nobody's software actually does.

Here's the claim, stripped of nuance: there's nothing stopping a GUI from being as keyboard-navigable as a TUI. Not the DOM, not the framework, not the platform. The only thing stopping it is that you didn't build it.

I want to be annoying about this because the HN thread proves the excuses don't hold up.

The excuse that died in the comments

"Most users prefer the mouse" is the go-to defense. It's also not the point. Nobody's arguing you rip out click targets. The argument is that when the mouse breaks, or your hands are full, or you're 40 windows deep in a workflow you do 200 times a day, keyboard-only should still get you to every action — not just copy/paste.

The thread's best anecdote: a user with a broken touchpad couldn't download the driver to fix the touchpad, because the download button only responded to clicks. That's not a hypothetical accessibility edge case. That's a GUI that fails at its own job the moment its primary input method degrades.

Another one: hotel and accounting software users filed complaints within hours when tab order broke in an update. Not because they're power users showing off vim motions — because eight hours a day of Tab → Tab → Enter is faster than eight hours a day of reaching for a mouse, and they'd built muscle memory around it. Break the tab order, break their throughput.

And the platform data point that should sting if you ship a Mac app: Tab navigation between UI controls is off by default on macOS. Windows apps from 25 years ago — built on Win32, no less — have better keyboard coverage than most SaaS products shipped this year. Excel is still nearly impossible to replace for power users, and keyboard efficiency is a real reason why.

"Accessible" and "fast" are the same feature

The highest-voted comment in the thread makes the point that matters most: keyboard support isn't a disability accommodation bolted onto the "real" product. It's the curb-cut effect — the ramp built for wheelchairs is also the thing parents use for strollers and delivery workers use for hand trucks.

Full keyboard support isn't for screen reader users. It's for:

  • the person whose trackpad just died
  • the person with a broken wrist typing one-handed
  • the power user who does the same 12-step flow 50 times a day
  • literally everyone, at 2am, when the mouse cursor decides to vanish

If you only build it for the first group, you build it badly, because you're treating it as compliance instead of UX. Build it because losing the mouse should never mean losing functionality, for anyone, and you end up with something that's actually fast to use.

It is genuinely not hard

This is the part that should make you uncomfortable. Nobody in that 395-comment thread produced a technical reason this is difficult. GNOME's own Human Interface Guidelines already say the quiet part out loud:

Every action should also be possible with the keyboard... users should be able to navigate and interact with every part of your user interface using only the keyboard.

That's not aspirational copy. It's a checklist. On the web, most of it is three ingredients:

<!-- 1. Native elements first. A <button> gets focus,
     Enter/Space activation, and role semantics for free. -->
<button onclick="save()">Save</button>

<!-- Not this — you're rebuilding what the browser already ships -->
<div onclick="save()">Save</div>
Enter fullscreen mode Exit fullscreen mode
// 2. If you MUST use a div (custom widget), wire it up yourself
const customButton = document.querySelector('.fake-button');
customButton.setAttribute('tabindex', '0');
customButton.setAttribute('role', 'button');
customButton.addEventListener('keydown', (e) => {
  if (e.key === 'Enter' || e.key === ' ') {
    e.preventDefault();
    save();
  }
});
Enter fullscreen mode Exit fullscreen mode
// 3. Trap focus in modals instead of leaking it to the page behind them
function trapFocus(modal) {
  const focusable = modal.querySelectorAll(
    'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
  );
  const first = focusable[0];
  const last = focusable[focusable.length - 1];

  modal.addEventListener('keydown', (e) => {
    if (e.key !== 'Tab') return;
    if (e.shiftKey && document.activeElement === first) {
      e.preventDefault();
      last.focus();
    } else if (!e.shiftKey && document.activeElement === last) {
      e.preventDefault();
      first.focus();
    }
  });
}
Enter fullscreen mode Exit fullscreen mode

That's it. That's the "hard" part. A logical tab order, native elements over div soup, and a focus trap for overlays. Every framework — React, Svelte, raw HTML — gets you 90% of this for free if you stop fighting it with <div onClick> everywhere.

The TUI myth, debunked in the same thread

Here's the twist that made me actually respect this HN thread instead of just skimming it: someone pointed out that TUIs aren't more accessible by default either. Screen readers choke on in-band signaling — the box-drawing characters and cursor tricks a TUI uses to fake a grid are just noise to assistive tech. A well-built native GUI has a semantic tree separate from the pixels, which is exactly the hook screen readers need. ARIA roles, accessible names, live regions — none of that exists in a terminal grid.

So the real hierarchy isn't "TUI good, GUI bad." It's: structured, semantic, keyboard-navigable beats unstructured, mouse-only, every time — and TUIs only look like they win because their entire input model forces developers to think about non-mouse navigation from line one. GUIs don't force that discipline, so most developers skip it.

What to actually do this week

You don't need a redesign. You need an audit:

  1. Unplug your mouse. Try to complete your app's core flow using only Tab, Shift+Tab, Enter, and arrow keys.
  2. Every place you got stuck is a bug, not an edge case.
  3. Check macOS: is "Tab navigates all controls" on by default in your web app's context, or are you relying on a system setting most users never touch?
  4. Grep your codebase for onClick on non-native elements. Each one is a keyboard trap waiting to be filed as a ticket by someone with a dead trackpad.

The HN crowd isn't asking you to build a vim-motion power-user toy. They're asking you to make Tab work. That bar is lower than you think, and most software still doesn't clear it.

Top comments (0)