JavaScript
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax and Types
-
Section 3: Strings and Numbers in Depth
-
Section 4: Control Flow
-
Section 5: Functions
-
Section 6: Objects and Arrays
-
Section 7: Maps, Sets, and Symbols
-
Section 8: Asynchronous JavaScript
-
Section 9: Object-Oriented and Prototypes
-
Section 10: The DOM
-
Section 11: Browser APIs
-
Section 12: Modern JavaScript (ES2015-ES2025)
-
Section 13: Functional Programming Patterns
-
Section 14: Error Handling and Debugging
-
Section 15: Testing
-
Section 16: Accessibility for JavaScript Developers
-
Section 17: Internationalization and Localization
-
Section 18: Performance
-
Section 19: Node.js Fundamentals
-
Section 20: Regular Expressions
-
Section 21: Design Patterns in JavaScript
-
Section 22: Security Basics
-
Section 23: Data Structures and Algorithms in JavaScript
-
Section 24: Practical Projects
-
Section 25: More Advanced Async Patterns
-
Section 26: More Object and Class Practice
-
Section 27: Working with Dates and Internationalization
-
Section 28: Web Components
-
Section 29: More DOM and Browser Practice
-
Section 30: Build Tooling for Vanilla JavaScript
-
Section 31: More Practice Projects
-
Section 32: Interview and Algorithm Practice
-
Section 33: Error Objects (MDN Reference)
-
Section 34: TypedArrays and Binary Data
-
Section 35: Reflection and Metaprogramming (MDN Reference)
-
Section 36: More Global Functions (MDN Reference)
131: Keyboard Navigation for Custom Widgets
We've spent a lot of time building things that look great and react quickly to clicks, but there's a silent failure point in most custom widgets: the keyboard. If you've ever tried to navigate a complex web app using only the Tab and Enter keys, you know exactly how frustrating it is when a "button" is actually just a div with a click listener. It's invisible to the keyboard, which means it's invisible to a huge segment of your users.
The "Click-Only" Trap
When I first started building custom UI components, my go-to move for a Tab switcher was simple. I'd make a list of div elements, style them to look like tabs, and attach a click event to each. In my head, it was elegant. The code was lean, and for me—using a mouse—it worked perfectly. I’d click "Settings," the content would swap, and I'd move on.
// The naive way: Just a click listener on a div
tabs.forEach(tab => {
tab.addEventListener('click', () => {
setActiveTab(tab);
});
});
The problem is that div elements aren't focusable by default. If a user hits the Tab key to move through your page, the browser skips right over your custom tabs. They might as well not exist. Even if you add tabindex="0" to make them focusable, you've only solved half the problem. A user can now reach the tab, but hitting "Enter" or "Space" won't do anything because you only listened for click. While some browsers synthesize a click event from an Enter key on a button, they don't do that for a div.
Why "Just Adding Tabindex" Isn't Enough
You might think the fix is just to make every tab focusable and add a keydown listener. But if you have five tabs, and the user has to Tab five times just to get past your navigation menu to reach the actual content, you've created a "tab trap" of tediousness. This is where we move from "making it work" to "making it professional."
In a standard accessible widget, the entire group of tabs should act as a single stop in the page's tab order. Once the user tabs into the tab list, they should use the arrow keys to move between the individual tabs. This is a pattern called "Roving Tabindex." I prefer this because it mirrors how native OS elements work; it keeps the page's main navigation flow lean while providing deep control within the widget.
Implementing Roving Tabindex and ARIA Roles
To do this right, we need to combine a few things: the correct ARIA roles so screen readers know this is a tab list, and a bit of logic to manage which element is currently "reachable."
First, we give the container role="tablist" and each tab role="tab". Then, we set tabindex="0" on the active tab and tabindex="-1" on all the others. An element with -1 can be focused programmatically via JavaScript, but the user can't Tab into it. This ensures that hitting Tab only lands the user on the currently selected tab.
// The professional way: Managing focus and keys
const tabs = document.querySelectorAll('[role="tab"]');
tabs.forEach((tab, index) => {
tab.addEventListener('keydown', (e) => {
let newIndex = null;
if (e.key === 'ArrowRight') {
newIndex = (index + 1) % tabs.length;
} else if (e.key === 'ArrowLeft') {
newIndex = (index - 1 + tabs.length) % tabs.length;
}
if (newIndex !== null) {
e.preventDefault(); // Stop the page from scrolling
tabs[newIndex].focus();
// Update tabindex: current becomes -1, new becomes 0
tab.setAttribute('tabindex', '-1');
tabs[newIndex].setAttribute('tabindex', '0');
setActiveTab(tabs[newIndex]);
}
});
});
I've added e.preventDefault() here because, by default, the arrow keys scroll the browser window. If your user is trying to navigate your tabs and the whole page starts sliding up and down, it feels broken. By handling the focus programmatically and updating the tabindex on the fly, we've created a widget that feels native and respects the user's input method.
📋 Practical Task
Build a Keyboard-Accessible Tab Switcher
Create a custom Tab component consisting of three tabs (e.g., "Profile", "Account", "Notifications") and three corresponding content panels. Your implementation must satisfy these specific requirements:
- ARIA Roles: The container must have
role="tablist"and each trigger must haverole="tab". - Roving Tabindex: Only the active tab should have
tabindex="0". All inactive tabs must havetabindex="-1". - Arrow Navigation: Users must be able to move focus between tabs using the Left and Right arrow keys.
- Focus Sync: When a user moves focus using the arrow keys, the corresponding content panel must be displayed, and the
tabindexmust be updated so the new tab is the only one reachable via the Tab key. - Activation: Ensure that both clicking a tab and pressing "Enter" or "Space" while a tab is focused activates that tab.
There are no comments for now.