DEV Community

Cover image for The shortcut that stole the text field
ANIRUDDHA ADAK
ANIRUDDHA ADAK

Posted on

The shortcut that stole the text field

The bug did not announce itself with an error screen. It waited for a keyboard.

I was building Comfort Paws Lab, a small React field guide with a three-step dog-comfort ritual. The central interaction was simple: choose a step, read its note, move on. I had made the buttons look like tabs, given them role="tab", updated aria-selected, and rendered one matching panel. With a pointer, it felt finished.

Then I stopped using the pointer.

Arrow Right did nothing. Arrow Down did nothing. Home and End did nothing. The focus indicator was there, sitting on a tab that was not active, while the content panel stayed somewhere else. I had made an interface that could describe itself as a tablist but could not behave like one.

That distinction became the whole debugging story. I did not have a rendering bug. I had a contract bug. The semantics I chose gave users and assistive technology a reasonable expectation, and my implementation had not met it.

The clue that changed the investigation

The first clue was not a stack trace. It was the feeling that tabbing through the control took too long for something with only three choices. That is the kind of friction a mouse user can miss because clicking collapses the distance between intent and result.

I reviewed the component in the order a keyboard experiences it. Which element receives focus first. What happens when focus moves. Which tab is in the tab order. Which panel is named by the current choice. The missing behavior became obvious once I stopped looking at the component as markup and started looking at it as a conversation.

The fix

I added a focused keyboard handler that recognizes Arrow Left, Arrow Right, Arrow Up, Arrow Down, Home, and End. It prevents the browser from scrolling when those keys are used for navigation, calculates the next index, updates state, and moves focus to the active tab.

const nextIndex = event.key === "Home"
  ? 0
  : event.key === "End"
    ? ritualOptions.length - 1
    : event.key === "ArrowRight" || event.key === "ArrowDown"
      ? (index + 1) % ritualOptions.length
      : (index - 1 + ritualOptions.length) % ritualOptions.length

setSelectedRitual(nextIndex)
document.getElementById(`ritual-tab-${nextIndex}`)?.focus()
Enter fullscreen mode Exit fullscreen mode

The rest of the change was smaller but just as important. Each tab got an aria-controls relationship, the tab order became roving, and the panel got an aria-labelledby reference to the active tab. The result is not flashy. It is a control that keeps its promise.

What I am proud of

I am proud that the fix made the interface more resilient without making it more complicated. There is no additional dependency, no large abstraction, and no fake test story. It is a few direct lines of React and a better understanding of what a tablist means.

I am also proud that the lesson survived the patch. The project is about noticing what makes a dog settle instead of assuming we already know. The debugging work asked for the same posture. Stop assuming the interface is finished because it looks finished. Ask it a different question. Use a different input. Notice what stays silent.

The repaired interaction is live in Comfort Paws Lab. The source-and-asset archive contains the exact working component and cover assets. If you have a favorite bug that only appeared when you changed the way you interacted with an interface, I would genuinely like to hear it.

Top comments (0)