Skip to Content
Course content

130: Managing Focus in JavaScript Applications

Click on the "Edit" button in the top corner of the screen to edit your slide content.

I was reviewing a PR for a junior dev last week, and they had built this slick search modal. Visually, it was perfect. But when I tried to use it with my keyboard—which is how I navigate most of the time—it was a nightmare. I'd click the search button, the modal would pop up, but my cursor was still stuck on the button behind the overlay. I had to tab three more times just to get into the input field. It felt broken.

Let's recreate that frustration and then fix it. We'll build a simple search toggle and see where the browser's focus actually goes.

The "Where am I?" Problem

Here is a basic setup: a button to open a search bar and a hidden input field. I'll write a quick script to toggle the visibility.

<button id="search-trigger">Open Search</button>
<div id="search-container" style="display: none;">
  <input type="text" id="search-input" placeholder="Search the site...">
  <button id="close-search">Close</button>
</div>

<script>
const trigger = document.getElementById('search-trigger');
const container = document.getElementById('search-container');
const closeBtn = document.getElementById('close-search');

trigger.onclick = () => {
  container.style.display = 'block';
};

closeBtn.onclick = () => {
  container.style.display = 'none';
};
</script>

If you run this and click "Open Search", the box appears. But try this: press the Tab key immediately after clicking. You'll notice the focus is still on the "Open Search" button, or it's jumping to some random link in the footer. The user is staring at a search box, but the browser thinks they are still interacting with the page behind it. That's a huge accessibility fail.

Forcing the Cursor's Hand

The obvious fix is to tell JavaScript exactly where the focus should be the moment the modal opens. We use the .focus() method. I'll update the trigger function.

trigger.onclick = () => {
  container.style.display = 'block';
  document.getElementById('search-input').focus();
};

That's better. Now, the second the modal opens, the cursor is blinking in the input field, ready for typing. It feels snappy. But we've just created a new problem. Try opening the search and then clicking "Close".

When the modal vanishes, where does the focus go? In most browsers, it just resets to the top of the body. If this was a long page and the search button was at the bottom, the user just got teleported back to the header. They've lost their place entirely. It's disorienting.

The Memory Trick

To fix this, we need to "remember" where the user was before they entered the modal. I'll create a variable to hold a reference to the element that triggered the action. This is a common pattern in professional UI libraries.

let lastFocusedElement;

trigger.onclick = () => {
  // Save the current active element before we move focus
  lastFocusedElement = document.activeElement; 
  
  container.style.display = 'block';
  document.getElementById('search-input').focus();
};

closeBtn.onclick = () => {
  container.style.display = 'none';
  
  // Put the focus back where it was
  if (lastFocusedElement) {
    lastFocusedElement.focus();
  }
};

Now the flow is seamless: Button → Input → Button. The user never feels like they've been ripped away from their original position on the page.

Dealing with the "Tab Trap"

There is one last annoyance. If I open the search modal and keep hitting Tab, I can actually tab out of the modal and start interacting with links in the background, even though the modal is visually covering the screen. This is called "focus leaking."

To solve this, we need a "focus trap." We listen for the keydown event and check if the user is hitting Tab. If they are at the last element of the modal, we wrap them back to the first element.

container.addEventListener('keydown', (e) => {
  if (e.key !== 'Tab') return;

  const focusables = container.querySelectorAll('input, button');
  const first = focusables[0];
  const last = focusables[focusables.length - 1];

  if (e.shiftKey) { // Shift + Tab (backward)
    if (document.activeElement === first) {
      last.focus();
      e.preventDefault();
    }
  } else { // Tab (forward)
    if (document.activeElement === last) {
      first.focus();
      e.preventDefault();
    }
  }
});

I'll be honest: writing focus traps from scratch every time is tedious. In a massive project, I'd use a library like Focus-Trap or an ARIA-compliant dialog component. But understanding the logic—saving the trigger, forcing focus, and trapping the Tab key—is what separates a "working" UI from a professional, accessible one.




📋 Practical Task

Exercise: The Accessible Settings Panel

Build a "Settings" panel for a user profile page. The page should have a "Settings" button and a hidden panel containing three different input fields (e.g., Username, Email, and a "Save Changes" button).

Your implementation must satisfy these three requirements:

  • Automatic Entry: When the Settings button is clicked, the panel must appear and focus must immediately move to the first input field.
  • Context Restoration: When the "Save Changes" or a "Cancel" button is clicked, the panel must hide and focus must return to the original "Settings" button.
  • The Tab Lock: While the panel is open, pressing Tab should cycle focus only between the inputs and the buttons inside the panel, preventing the user from tabbing into the main page content.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.